From 4d3115d7b690b678c45a564eec5b5bf123ebb8ca Mon Sep 17 00:00:00 2001 From: Ivan <210490533+VZaphod@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:11:43 +0200 Subject: [PATCH 01/19] ConnectionServices Kotlin sample --- .../.gitignore | 19 + .../app/.gitignore | 1 + .../app/build.gradle.kts | 66 ++++ .../app/proguard-rules.pro | 21 ++ .../ExampleInstrumentedTest.kt | 24 ++ .../app/src/main/AndroidManifest.xml | 74 ++++ .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 14250 bytes .../Call.kt | 17 + .../CallActionReceiver.kt | 77 ++++ .../CallHolder.kt | 54 +++ .../MainActivity.kt | 159 ++++++++ .../MyApp.kt | 23 ++ .../NotificationChannelManager.kt | 57 +++ .../OpenTokConfig.kt | 43 +++ .../VonageManager.kt | 210 +++++++++++ .../connectionservice/PhoneAccountManager.kt | 136 +++++++ .../connectionservice/VonageConnection.kt | 350 ++++++++++++++++++ .../VonageConnectionHolder.kt | 8 + .../VonageConnectionService.kt | 184 +++++++++ .../AudioDeviceSelectionListener.kt | 9 + .../deviceselector/AudioDeviceSelector.kt | 149 ++++++++ .../AudioDeviceSelectorDialog.kt | 67 ++++ .../di/AppModule.kt | 89 +++++ .../home/HomeView.kt | 48 +++ .../home/HomeViewModel.kt | 28 ++ .../room/PublisherView.kt | 26 ++ .../room/RoomView.kt | 176 +++++++++ .../room/RoomViewModel.kt | 76 ++++ .../room/SubscriberGrid.kt | 59 +++ .../ui/theme/Color.kt | 11 + .../ui/theme/Theme.kt | 58 +++ .../ui/theme/Type.kt | 34 ++ .../usecases/EndCallUseCase.kt | 11 + .../usecases/StartIncomingCallUseCase.kt | 18 + .../usecases/StartOutgoingCallUseCase.kt | 18 + .../usecases/UnholdUseCase.kt | 11 + .../app/src/main/res/drawable/answer_call.xml | 9 + .../app/src/main/res/drawable/end_call.xml | 9 + .../res/drawable/ic_launcher_background.xml | 170 +++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ .../res/drawable/ic_stat_ic_notification.png | Bin 0 -> 855 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 818 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 670 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2060 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 594 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 438 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1308 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1058 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 896 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 2922 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 1592 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 1342 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 4556 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 2176 bytes .../ic_launcher_foreground.webp | Bin 0 -> 1842 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 6560 bytes .../app/src/main/res/values/colors.xml | 10 + .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 5 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../ExampleUnitTest.kt | 17 + .../build.gradle.kts | 8 + .../gradle.properties | 23 ++ .../gradle/libs.versions.toml | 39 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../gradlew | 185 +++++++++ .../gradlew.bat | 89 +++++ .../settings.gradle.kts | 24 ++ 73 files changed, 3084 insertions(+) create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/.gitignore create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/.gitignore create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/build.gradle.kts create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/proguard-rules.pro create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/androidTest/java/com/vonage/basic_video_chat_connectionservice/ExampleInstrumentedTest.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/AndroidManifest.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/ic_launcher-playstore.png create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/Call.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/CallActionReceiver.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/CallHolder.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/MainActivity.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/MyApp.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/NotificationChannelManager.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/OpenTokConfig.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/VonageManager.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/PhoneAccountManager.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnection.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnectionHolder.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnectionService.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelectionListener.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelector.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelectorDialog.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/di/AppModule.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/home/HomeView.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/home/HomeViewModel.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/PublisherView.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/RoomView.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/RoomViewModel.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/SubscriberGrid.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Color.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Theme.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Type.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/EndCallUseCase.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/StartIncomingCallUseCase.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/StartOutgoingCallUseCase.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/UnholdUseCase.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/answer_call.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/end_call.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_stat_ic_notification.png create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/colors.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/ic_launcher_background.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/strings.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/themes.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/xml/backup_rules.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/app/src/test/java/com/vonage/basic_video_chat_connectionservice/ExampleUnitTest.kt create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/build.gradle.kts create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/gradle.properties create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/gradle/libs.versions.toml create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/gradle/wrapper/gradle-wrapper.jar create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/gradle/wrapper/gradle-wrapper.properties create mode 100755 Basic-Video-Chat-ConnectionService-Kotlin/gradlew create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/gradlew.bat create mode 100644 Basic-Video-Chat-ConnectionService-Kotlin/settings.gradle.kts diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/.gitignore b/Basic-Video-Chat-ConnectionService-Kotlin/.gitignore new file mode 100644 index 00000000..36df0d89 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/.gitignore @@ -0,0 +1,19 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +app/build + +.settings/ +app/jniLibs/ diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/.gitignore b/Basic-Video-Chat-ConnectionService-Kotlin/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/build.gradle.kts b/Basic-Video-Chat-ConnectionService-Kotlin/app/build.gradle.kts new file mode 100644 index 00000000..ebbd91a6 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) + alias(libs.plugins.google.dagger.hilt) +} + +android { + namespace = "com.vonage.basic_video_chat_connectionservice" + compileSdk = 36 + + defaultConfig { + applicationId = "com.vonage.basic_video_chat_connectionservice" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(files("libs/opentok-android-sdk-2.31.0.aar")) + implementation(libs.vonage.webrtc) + implementation(libs.guava) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/proguard-rules.pro b/Basic-Video-Chat-ConnectionService-Kotlin/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/androidTest/java/com/vonage/basic_video_chat_connectionservice/ExampleInstrumentedTest.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/androidTest/java/com/vonage/basic_video_chat_connectionservice/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..37588ca3 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/androidTest/java/com/vonage/basic_video_chat_connectionservice/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.vonage.basic_video_chat_connectionservice + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.vonage.basic_video_chat_connectionservice", appContext.packageName) + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/AndroidManifest.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d3d01fdf --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/ic_launcher-playstore.png b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..87bd88304df1e271948fec556131f3519243a5be GIT binary patch literal 14250 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 literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/Call.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/Call.kt new file mode 100644 index 00000000..6e90e1eb --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/Call.kt @@ -0,0 +1,17 @@ +package com.vonage.basic_video_chat_connectionservice + +data class Call( + val callID: Int, + val name: String, + val state: CallState +) + +enum class CallState { + IDLE, + CONNECTING, + DIALING, + ANSWERING, + CONNECTED, + HOLDING, + DISCONNECTED +} diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/CallActionReceiver.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/CallActionReceiver.kt new file mode 100644 index 00000000..a652ed4f --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/CallActionReceiver.kt @@ -0,0 +1,77 @@ +package com.vonage.basic_video_chat_connectionservice + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.vonage.basic_video_chat_connectionservice.connectionservice.VonageConnectionHolder + +class CallActionReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + if (intent != null && intent.action != null) { + val action = intent.action + when (action) { + ACTION_ANSWER_CALL -> { + Log.d(TAG, "Action: Answer call") + answerCall() + } + + ACTION_REJECT_CALL -> { + Log.d(TAG, "Action: Reject call") + rejectCall() + } + + ACTION_END_CALL -> { + Log.d(TAG, "Action: End call") + endCall() + } + + else -> Log.w( + TAG, + "Unknown action: $action" + ) + } + } + } + + private fun answerCall() { + VonageConnectionHolder.connection?.onAnswer() + } + + private fun rejectCall() { + VonageConnectionHolder.connection?.onReject() + } + + private fun endCall() { + VonageConnectionHolder.connection?.onDisconnect() + } + + companion object { + private const val TAG = "CallActionReceiver" + + const val ACTION_ANSWER_CALL: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_ANSWER_CALL" + const val ACTION_REJECT_CALL: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_REJECT_CALL" + const val ACTION_END_CALL: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_END_CALL" + const val ACTION_ANSWERED_CALL: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_ANSWERED_CALL" + const val ACTION_INCOMING_CALL: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_INCOMING_CALL" + const val ACTION_NOTIFY_INCOMING_CALL: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_NOTIFY_INCOMING_CALL" + const val ACTION_REJECTED_CALL: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_REJECTED_CALL" + const val ACTION_CALL_ENDED: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_CALL_ENDED" + const val ACTION_CALL_HOLDING: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_CALL_HOLDING" + const val ACTION_CALL_UNHOLDING: String = + "com.tokbox.sample.basicvideochatconnectionservice.ACTION_CALL_UNHOLDING" + + const val ACTION_ANSWER_CALL_ID: Int = 2 + const val ACTION_REJECT_CALL_ID: Int = 3 + const val ACTION_END_CALL_ID: Int = 4 + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/CallHolder.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/CallHolder.kt new file mode 100644 index 00000000..7c095153 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/CallHolder.kt @@ -0,0 +1,54 @@ +package com.vonage.basic_video_chat_connectionservice + +import android.view.View +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class CallHolder { + private val _callFlow = MutableStateFlow(null) + val callFlow: Flow = _callFlow.asStateFlow() + + private val _callStateFlow = MutableStateFlow(CallState.IDLE) + val callStateFlow: Flow = _callStateFlow.asStateFlow() + + private val subscriberMap = mutableMapOf() + + private val _publisherViewFlow = MutableStateFlow(null) + val publisherViewFlow: Flow = _publisherViewFlow.asStateFlow() + + private val _subscriberViewFlow = MutableStateFlow>(emptyList()) + val subscriberViewFlow: Flow> = _subscriberViewFlow.asStateFlow() + + fun setCall(call: Call?) { + _callFlow.value = call + } + + fun updateCallState(state: CallState) { + _callStateFlow.value = state + } + + fun setPublisherView(view: View?) { + _publisherViewFlow.value = view + } + + fun addSubscriberView(streamId: String, view: View) { + subscriberMap[streamId] = view + + _subscriberViewFlow.value = subscriberMap.values.toList() + } + + fun removeSubscriberView(streamId: String) { + subscriberMap.remove(streamId) + + _subscriberViewFlow.value = subscriberMap.values.toList() + } + + fun clear() { + _callFlow.value = null + _callStateFlow.value = CallState.IDLE + subscriberMap.clear() + _publisherViewFlow.value = null + _subscriberViewFlow.value = emptyList() + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/MainActivity.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/MainActivity.kt new file mode 100644 index 00000000..6ca32ef8 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/MainActivity.kt @@ -0,0 +1,159 @@ +package com.vonage.basic_video_chat_connectionservice + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.core.app.ActivityCompat +import androidx.core.net.toUri +import com.vonage.basic_video_chat_connectionservice.deviceselector.AudioDeviceSelector +import com.vonage.basic_video_chat_connectionservice.deviceselector.AudioDeviceSelectorDialog +import com.vonage.basic_video_chat_connectionservice.home.HomeView +import com.vonage.basic_video_chat_connectionservice.home.HomeViewModel +import com.vonage.basic_video_chat_connectionservice.room.RoomView +import com.vonage.basic_video_chat_connectionservice.room.RoomViewModel +import com.vonage.basic_video_chat_connectionservice.ui.theme.BasicVideoChatConnectionServiceKotlinTheme +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + private val homeViewModel: HomeViewModel by viewModels() + private val roomViewModel: RoomViewModel by viewModels() + + @Inject + lateinit var powerManager: PowerManager + @Inject + lateinit var vonageManager: VonageManager + @Inject + lateinit var callHolder: CallHolder + @Inject + lateinit var audioDeviceSelector: AudioDeviceSelector + + val isBatteryOptimizationIgnored: Boolean + get() = powerManager.isIgnoringBatteryOptimizations(applicationContext.packageName) + + private val permissionLauncher: ActivityResultLauncher> = + registerForActivityResult(RequestMultiplePermissions()) { result -> + for ((permission, isGranted) in result) { + Log.d("Permission", "$permission -> ${if (isGranted) "GRANTED" else "DENIED"}") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + requestPermissions() + + enableEdgeToEdge() + setContent { + val call by callHolder.callFlow.collectAsState(initial = null) + var showAudioDeviceSelector by remember { mutableStateOf(false) } + + BasicVideoChatConnectionServiceKotlinTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + if (call != null) { + RoomView( + roomViewModel = roomViewModel, + onShowAudioDevicesClick = { + showAudioDeviceSelector = true + }) + } else { + HomeView( + homeViewModel = homeViewModel, + modifier = Modifier.padding(innerPadding)) + } + + if (showAudioDeviceSelector) { + AudioDeviceSelectorDialog( + audioDeviceSelector = audioDeviceSelector, + onDismissRequest = { + showAudioDeviceSelector = false + } + ) + } + } + } + } + } + + private fun requestPermissions() { + val perms = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> arrayOf( + Manifest.permission.INTERNET, + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CALL_PHONE, + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.POST_NOTIFICATIONS, + Manifest.permission.MANAGE_OWN_CALLS + ) + else -> arrayOf( + Manifest.permission.INTERNET, + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CALL_PHONE + ) + } + + val permissionsNeeded = perms.any { permission -> + ActivityCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED + } + + if (permissionsNeeded) { + permissionLauncher.launch(perms) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && !isBatteryOptimizationIgnored) { + requestBatteryOptimizationIntent() + } + } + + private fun requestBatteryOptimizationIntent() { + val packageName = applicationContext.packageName + + if (!powerManager.isIgnoringBatteryOptimizations(packageName)) { + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = "package:$packageName".toUri() + } + startActivity(intent) + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + public override fun onResume() { + super.onResume() + vonageManager.onResume() + } + + public override fun onPause() { + super.onPause() + vonageManager.onPause() + } + + public override fun onDestroy() { + super.onDestroy() + vonageManager.endSession() + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/MyApp.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/MyApp.kt new file mode 100644 index 00000000..74fdc000 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/MyApp.kt @@ -0,0 +1,23 @@ +package com.vonage.basic_video_chat_connectionservice + +import android.app.Application +import android.os.Build +import com.vonage.basic_video_chat_connectionservice.connectionservice.PhoneAccountManager +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject + +@HiltAndroidApp +class MyApp: Application() { + @Inject lateinit var phoneAccountManager: PhoneAccountManager + @Inject lateinit var notificationChannelManager: NotificationChannelManager + + override fun onCreate() { + super.onCreate() + + phoneAccountManager.registerPhoneAccount() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notificationChannelManager.setup() + } + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/NotificationChannelManager.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/NotificationChannelManager.kt new file mode 100644 index 00000000..605ce15d --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/NotificationChannelManager.kt @@ -0,0 +1,57 @@ +package com.vonage.basic_video_chat_connectionservice + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.media.AudioAttributes +import android.media.RingtoneManager +import android.os.Build +import androidx.annotation.RequiresApi + +class NotificationChannelManager(private val context: Context) { + + @RequiresApi(api = Build.VERSION_CODES.O) + fun setup() { + setupIncomingCallChannel() + setupOngoingCallChannel() + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private fun setupIncomingCallChannel() { + val channel = NotificationChannel( + INCOMING_CALL_CHANNEL_ID, "Incoming Calls", + NotificationManager.IMPORTANCE_HIGH + ) + + val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + channel.setSound( + ringtoneUri, AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + ) + + val mgr = context.getSystemService( + NotificationManager::class.java + ) + mgr.createNotificationChannel(channel) + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private fun setupOngoingCallChannel() { + val channel = NotificationChannel( + ONGOING_CALL_CHANNEL_ID, "Ongoing Calls", + NotificationManager.IMPORTANCE_DEFAULT + ) + + val mgr = context.getSystemService( + NotificationManager::class.java + ) + mgr.createNotificationChannel(channel) + } + + companion object { + const val INCOMING_CALL_CHANNEL_ID: String = "vonage_video_call_channel" + const val ONGOING_CALL_CHANNEL_ID: String = "vonage_ongoing_video_call_channel" + } +} diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/OpenTokConfig.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/OpenTokConfig.kt new file mode 100644 index 00000000..aa39c0c1 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/OpenTokConfig.kt @@ -0,0 +1,43 @@ +package com.vonage.basic_video_chat_connectionservice + +import android.text.TextUtils + +object OpenTokConfig { + /* + Fill the following variables using your own Project info from the OpenTok dashboard + https://dashboard.tokbox.com/projects + + Note that this application will ignore credentials in the `OpenTokConfig` file when `CHAT_SERVER_URL` contains a + valid URL. + */ + // Replace with a API key + const val API_KEY: String = "" + + // Replace with a generated Session ID + const val SESSION_ID: String = "" + + // Replace with a generated token (from the dashboard or using an OpenTok server SDK) + const val TOKEN: String = "" + + val isValid: Boolean + // *** The code below is to validate this configuration file. You do not need to modify it *** + get() { + if (TextUtils.isEmpty(API_KEY) + || TextUtils.isEmpty(SESSION_ID) + || TextUtils.isEmpty(TOKEN) + ) { + return false + } + + return true + } + + val description: String + get() = (""" + OpenTokConfig: + API_KEY: $API_KEY + SESSION_ID: $SESSION_ID + TOKEN: $TOKEN + + """.trimIndent()) +} diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/VonageManager.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/VonageManager.kt new file mode 100644 index 00000000..8a225207 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/VonageManager.kt @@ -0,0 +1,210 @@ +package com.vonage.basic_video_chat_connectionservice + +import android.content.Context +import android.opengl.GLSurfaceView +import android.util.Log +import com.opentok.android.AudioDeviceManager +import com.opentok.android.BaseAudioDevice.AudioFocusManager +import com.opentok.android.BaseVideoRenderer +import com.opentok.android.OpentokError +import com.opentok.android.Publisher +import com.opentok.android.PublisherKit +import com.opentok.android.PublisherKit.PublisherListener +import com.opentok.android.Session +import com.opentok.android.Stream +import com.opentok.android.Subscriber +import com.opentok.android.SubscriberKit +import com.opentok.android.SubscriberKit.SubscriberListener +import com.vonage.basic_video_chat_connectionservice.connectionservice.VonageConnectionHolder + +class VonageManager( + private val context: Context, + private val audioDeviceManager: AudioDeviceManager, + private val callHolder: CallHolder +) { + private var session: Session? = null + private var publisher: Publisher? = null + private var subscriber: Subscriber? = null + private var audioFocusManager: AudioFocusManager? = null + + private val publisherListener: PublisherListener = object : PublisherListener { + override fun onStreamCreated(publisherKit: PublisherKit, stream: Stream) { + Log.d(TAG, "onStreamCreated: Publisher Stream Created. Own stream " + stream.streamId) + } + + override fun onStreamDestroyed(publisherKit: PublisherKit, stream: Stream) { + Log.d( + TAG, + "onStreamDestroyed: Publisher Stream Destroyed. Own stream " + stream.streamId + ) + } + + override fun onError(publisherKit: PublisherKit, opentokError: OpentokError) { + //callback?.onError("PublisherKit onError: " + opentokError.message) + } + } + + private val sessionListener: Session.SessionListener = object : Session.SessionListener { + override fun onConnected(session: Session) { + Log.d(TAG, "onConnected: Connected to session: " + session.sessionId) + + if (publisher != null) { + publisher!!.destroy() + } + + publisher = Publisher.Builder(context).build() + publisher!!.setPublisherListener(publisherListener) + publisher!!.renderer + .setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL) + + callHolder.setPublisherView(publisher!!.view) + + if (publisher!!.view is GLSurfaceView) { + (publisher!!.view as GLSurfaceView).setZOrderOnTop(true) + } + + session.publish(publisher) + } + + override fun onDisconnected(session: Session) { + Log.d(TAG, "onDisconnected: Disconnected from session: " + session.sessionId) + + callHolder.setPublisherView(null) + } + + override fun onStreamReceived(session: Session, stream: Stream) { + Log.d( + TAG, + "onStreamReceived: New Stream Received " + stream.streamId + " in session: " + session.sessionId + ) + + if (subscriber == null) { + subscriber = Subscriber.Builder(context, stream).build() + subscriber!!.renderer.setStyle( + BaseVideoRenderer.STYLE_VIDEO_SCALE, + BaseVideoRenderer.STYLE_VIDEO_FILL + ) + subscriber!!.setSubscriberListener(subscriberListener) + session.subscribe(subscriber) + callHolder.addSubscriberView(streamId = stream.streamId, view = subscriber!!.view) + } + } + + override fun onStreamDropped(session: Session, stream: Stream) { + Log.d( + TAG, + "onStreamDropped: Stream Dropped: " + stream.streamId + " in session: " + session.sessionId + ) + + callHolder.removeSubscriberView(streamId = stream.streamId) + } + + override fun onError(session: Session, opentokError: OpentokError) { + //callback?.onError("Session error: " + opentokError.message) + } + } + + var subscriberListener: SubscriberListener = object : SubscriberListener { + override fun onConnected(subscriberKit: SubscriberKit) { + Log.d( + TAG, + "onConnected: Subscriber connected. Stream: " + subscriberKit.stream.streamId + ) + } + + override fun onDisconnected(subscriberKit: SubscriberKit) { + Log.d( + TAG, + "onDisconnected: Subscriber disconnected. Stream: " + subscriberKit.stream.streamId + ) + } + + override fun onError(subscriberKit: SubscriberKit, opentokError: OpentokError) { + //callback?.onError("SubscriberKit onError: " + opentokError.message) + } + } + + fun initializeSession(apiKey: String, sessionId: String, token: String) { + Log.i(TAG, "apiKey: $apiKey") + Log.i(TAG, "sessionId: $sessionId") + Log.i(TAG, "token: $token") + + session = Session.Builder(context.applicationContext, apiKey, sessionId).build() + session!!.setSessionListener(sessionListener) + session!!.connect(token) + } + + fun onResume() { + if (session != null) session!!.onResume() + } + + fun onPause() { + if (session != null) session!!.onPause() + } + + fun endSession() { + callHolder.clear() + + if (subscriber != null) { + if (session != null) { + session!!.unsubscribe(subscriber) + } + } + + if (publisher != null) { + if (session != null) { + session!!.unpublish(publisher) + } + publisher!!.destroy() + } + + + if (session != null) { + session!!.disconnect() + } + + session = null + publisher = null + subscriber = null + } + + fun setAudioFocusManager() { + audioFocusManager = audioDeviceManager.audioFocusManager + + if (audioFocusManager == null) { + throw RuntimeException("Audio Focus Manager should have been granted") + } else { + audioFocusManager!!.setRequestAudioFocus(false) + } + } + + fun notifyAudioFocusIsActive() { + Log.d("VonageCallManager", "notifyAudioFocusIsActive() called") + if (audioFocusManager == null) { + throw RuntimeException("Audio Focus Manager should have been granted") + } + audioFocusManager!!.audioFocusActivated() + } + + fun notifyAudioFocusIsInactive() { + Log.d("VonageCallManager", "notifyAudioFocusIsInactive() called") + if (audioFocusManager == null) { + throw RuntimeException("Audio Focus Manager should have been granted") + } + audioFocusManager!!.audioFocusDeactivated() + } + + fun endCall() { + VonageConnectionHolder.connection?.onDisconnect() + } + + fun setMuted(isMuted: Boolean) { + if (publisher != null) { + publisher!!.publishAudio = !isMuted + } + } + + companion object { + private val TAG: String = VonageManager::class.java.simpleName + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/PhoneAccountManager.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/PhoneAccountManager.kt new file mode 100644 index 00000000..f6a95f70 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/PhoneAccountManager.kt @@ -0,0 +1,136 @@ +package com.vonage.basic_video_chat_connectionservice.connectionservice + +import android.Manifest +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.Color +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.telecom.PhoneAccount +import android.telecom.PhoneAccountHandle +import android.telecom.TelecomManager +import android.telecom.VideoProfile +import android.util.Log +import androidx.core.app.ActivityCompat +import com.vonage.basic_video_chat_connectionservice.R + +class PhoneAccountManager( + private val context: Context, + private val telecomManager: TelecomManager +) { + var handle: PhoneAccountHandle? = null + + fun registerPhoneAccount() { + + val componentName: ComponentName = + ComponentName(context, VonageConnectionService::class.java) + handle = PhoneAccountHandle(componentName, ACCOUNT_ID) + val phoneAccount = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PhoneAccount.builder(handle, "Vonage Video") + .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED or PhoneAccount.CAPABILITY_VIDEO_CALLING) + .setHighlightColor(Color.BLUE) + .setIcon(Icon.createWithResource(context, R.mipmap.ic_launcher)) + .addSupportedUriScheme(PhoneAccount.SCHEME_TEL) + .addSupportedUriScheme(VONAGE_CALL_SCHEME) + .build() + } else { + PhoneAccount.builder(handle, "Vonage Video") + .setCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING) + .setHighlightColor(Color.BLUE) + .setIcon(Icon.createWithResource(context, R.mipmap.ic_launcher)) + .addSupportedUriScheme(PhoneAccount.SCHEME_TEL) + .addSupportedUriScheme(VONAGE_CALL_SCHEME) + .build() + } + + telecomManager.registerPhoneAccount(phoneAccount) + Log.d("PhoneAccountManager", "PhoneAccount registered: " + phoneAccount.isEnabled) + } + + fun startOutgoingVideoCall(callerName: String?, callerId: String?) { + val extras = Bundle() + extras.putString(CALLER_NAME, callerName) + extras.putString(CALLER_ID, callerId) + extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle) + extras.putInt( + TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, + VideoProfile.STATE_BIDIRECTIONAL + ) + + val calleeUri = Uri.Builder() + .scheme(VONAGE_CALL_SCHEME) + .authority(callerId) + .appendQueryParameter("callerName", callerName) + .build() + + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.CALL_PHONE + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + telecomManager.placeCall(calleeUri, extras) + } + + fun notifyIncomingVideoCall(callerName: String?, callerId: String?) { + val extras = Bundle() + extras.putString(CALLER_NAME, callerName) + extras.putString(CALLER_ID, callerId) + extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle) + + val calleeUri = Uri.Builder() + .scheme(VONAGE_CALL_SCHEME) + .authority(callerId) + .appendQueryParameter("callerName", callerName) + .build() + + extras.putString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, calleeUri.toString()) + + if (handle != null) { + telecomManager.addNewIncomingCall(handle, extras) + Log.d("PhoneAccountManager", "Incoming video call notified.") + } else { + Log.e( + "PhoneAccountManager", + "TelecomManager or PhoneAccountHandle is null. Cannot notify incoming call." + ) + } + } + + fun canPlaceIncomingCall(): Boolean { + if (handle == null) { + Log.e("PhoneAccountManager", "TelecomManager or PhoneAccountHandle is not initialized.") + return false + } + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + telecomManager.isIncomingCallPermitted(handle) + } else { + true + } + } + + fun canPlaceOutgoingCall(): Boolean { + if (handle == null) { + Log.e("PhoneAccountManager", "TelecomManager or PhoneAccountHandle is not initialized.") + return false + } + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + telecomManager.isOutgoingCallPermitted(handle) + } else { + true + } + } + + companion object { + const val ACCOUNT_ID: String = "vonage_video_call" + private const val VONAGE_CALL_SCHEME = "vonagecall" + var CALLER_NAME: String = "CALLER_NAME" + var CALLER_ID: String = "CALLER_ID" + } +} diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnection.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnection.kt new file mode 100644 index 00000000..b78a252a --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnection.kt @@ -0,0 +1,350 @@ +package com.vonage.basic_video_chat_connectionservice.connectionservice + +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Person +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.OutcomeReceiver +import android.telecom.CallAudioState +import android.telecom.CallEndpoint +import android.telecom.CallEndpointException +import android.telecom.Connection +import android.telecom.DisconnectCause +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import com.vonage.basic_video_chat_connectionservice.CallActionReceiver +import com.vonage.basic_video_chat_connectionservice.CallHolder +import com.vonage.basic_video_chat_connectionservice.CallState +import com.vonage.basic_video_chat_connectionservice.MainActivity +import com.vonage.basic_video_chat_connectionservice.NotificationChannelManager +import com.vonage.basic_video_chat_connectionservice.OpenTokConfig.API_KEY +import com.vonage.basic_video_chat_connectionservice.OpenTokConfig.SESSION_ID +import com.vonage.basic_video_chat_connectionservice.OpenTokConfig.TOKEN +import com.vonage.basic_video_chat_connectionservice.R +import com.vonage.basic_video_chat_connectionservice.VonageManager +import com.vonage.basic_video_chat_connectionservice.deviceselector.AudioDeviceSelectionListener +import com.vonage.basic_video_chat_connectionservice.deviceselector.AudioDeviceSelector + +class VonageConnection( + private val context: Context, + private val audioDeviceSelector: AudioDeviceSelector, + private val vonageManager: VonageManager, + private val callHolder: CallHolder, + private val remoteName: String, + private val callNotificationId: Int +): Connection(), AudioDeviceSelectionListener { + + var onCallEnded: (()->Unit)? = null + + override fun onSilence() { + super.onSilence() + Log.d(TAG, "onSilence") + + postIncomingCallNotification(false) + } + + fun onPlaceCall() { + Log.d(TAG, "onPlaceCall") + setActive() + vonageManager.initializeSession(API_KEY, SESSION_ID, TOKEN) + + callHolder.updateCallState(CallState.DIALING) + } + + override fun onAnswer() { + super.onAnswer() + Log.d(TAG, "onAnswer") + + setActive() + vonageManager.initializeSession(API_KEY, SESSION_ID, TOKEN) + postIncomingCallNotification(false) + updateOngoingCallNotification() + + callHolder.updateCallState(CallState.ANSWERING) + } + + override fun onDisconnect() { + super.onDisconnect() + Log.d(TAG, "onDisconnect") + + callHolder.updateCallState(CallState.DISCONNECTED) + + vonageManager.endSession() + audioDeviceSelector.listener = null + setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) + + destroy() + + onCallEnded?.invoke() + } + + override fun onAbort() { + super.onAbort() + Log.d(TAG, "onAbort") + + onDisconnect() + } + + override fun onReject() { + super.onReject() + Log.d(TAG, "onReject") + + callHolder.updateCallState(CallState.DISCONNECTED) + + setDisconnected(DisconnectCause(DisconnectCause.REJECTED)) + + destroy() + + onCallEnded?.invoke() + } + + override fun onHold() { + super.onHold() + Log.d(TAG, "onHold") + + setOnHold() + vonageManager.setMuted(true) + + callHolder.updateCallState(CallState.HOLDING) + } + + override fun onUnhold() { + super.onUnhold() + Log.d(TAG, "onUnhold") + + setActive() + vonageManager.setMuted(false) + + callHolder.updateCallState(CallState.CONNECTED) + } + + override fun onStateChanged(state: Int) { + super.onStateChanged(state) + Log.d(TAG, "onStateChanged " + stateToString(state)) + } + + override fun onAvailableCallEndpointsChanged(endpoints: List) { + super.onAvailableCallEndpointsChanged(endpoints) + Log.d(TAG, "onAvailableCallEndpointsChanged") + + audioDeviceSelector.onAvailableCallEndpointsChanged(endpoints) + } + + override fun onCallEndpointChanged(endpoint: CallEndpoint) { + super.onCallEndpointChanged(endpoint) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + Log.d("VonageConnection", "Active audio endpoint changed to: " + endpoint.endpointType) + } + + audioDeviceSelector.onCallEndpointChanged(endpoint) + } + + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private fun changeCallEndpoint(endpoint: CallEndpoint) { + val executor = ContextCompat.getMainExecutor(context) + + requestCallEndpointChange( + endpoint, + executor, + object : OutcomeReceiver { + override fun onResult(result: Void?) { + Log.d( + "VonageConnection", + "Successfully switched to endpoint: " + endpoint.endpointType + ) + } + + override fun onError(error: CallEndpointException) { + Log.e("VonageConnection", "Failed to switch endpoint: " + error.message) + } + }) + } + + @Deprecated("Deprecated in Java") + override fun onCallAudioStateChanged(state: CallAudioState?) { + super.onCallAudioStateChanged(state) + + Log.d("VonageConnection", "Current audio route: " + state?.route) + + state?.let { + audioDeviceSelector.onCallAudioStateChanged(it) + } + } + + + override fun onAudioDeviceSelected(device: AudioDeviceSelector.AudioDevice) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + device.endpoint?.let { + changeCallEndpoint(it) + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setAudioRoute(device.type) + } + } + + override fun onMuteStateChanged(isMuted: Boolean) { + super.onMuteStateChanged(isMuted) + Log.d(TAG, "onMuteStateChanged") + + vonageManager.setMuted(isMuted) + } + + private fun postIncomingCallNotification(isRinging: Boolean) { + val notification = getIncomingCallNotification(isRinging) + notification.flags = notification.flags or Notification.FLAG_INSISTENT + val notificationManager = context.getSystemService( + NotificationManager::class.java + ) + notificationManager.notify(callNotificationId, notification) + } + + fun getIncomingCallNotification(isRinging: Boolean): Notification { + // Create an intent which triggers your fullscreen incoming call user interface. + val intent = Intent(Intent.ACTION_MAIN, null) + intent.flags = Intent.FLAG_ACTIVITY_NO_USER_ACTION or Intent.FLAG_ACTIVITY_NEW_TASK + intent.setClass(context, MainActivity::class.java) + val pendingIntent = + PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_IMMUTABLE) + + // Build the notification as an ongoing high priority item; this ensures it will show as + // a heads up notification which slides down over top of the current content. + val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Notification.Builder(context, NotificationChannelManager.INCOMING_CALL_CHANNEL_ID) + } else { + Notification.Builder(context) + } + builder.setOngoing(true) + builder.setPriority(Notification.PRIORITY_HIGH) + builder.setOnlyAlertOnce(!isRinging) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setColorized(true) + } + // Set notification content intent to take user to fullscreen UI if user taps on the + // notification body. + builder.setContentIntent(pendingIntent) + // Set full screen intent to trigger display of the fullscreen UI when the notification + // manager deems it appropriate. + builder.setFullScreenIntent(pendingIntent, true) + + builder.setSmallIcon(R.drawable.ic_stat_ic_notification) + builder.setContentTitle("Incoming call") + builder.setContentText("$remoteName is calling...") + builder.setColor(-0xde690d) + + val answerIntent = Intent( + context, + CallActionReceiver::class.java + ) + answerIntent.action = CallActionReceiver.ACTION_ANSWER_CALL + val answerPendingIntent = PendingIntent.getBroadcast( + context, + CallActionReceiver.ACTION_ANSWER_CALL_ID, + answerIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + val rejectIntent = Intent( + context, + CallActionReceiver::class.java + ) + rejectIntent.action = CallActionReceiver.ACTION_REJECT_CALL + val rejectPendingIntent = PendingIntent.getBroadcast( + context, + CallActionReceiver.ACTION_REJECT_CALL_ID, + rejectIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val caller = Person.Builder() + .setName(remoteName) + .setImportant(true) + .build() + builder.style = Notification.CallStyle.forIncomingCall( + caller, + rejectPendingIntent, + answerPendingIntent + ) + } else { + builder.addAction( + Notification.Action.Builder( + R.drawable.answer_call, "Answer", answerPendingIntent + ).build() + ) + builder.addAction( + Notification.Action.Builder( + R.drawable.end_call, "Reject", rejectPendingIntent + ).build() + ) + } + + return builder.build() + } + + private fun updateOngoingCallNotification() { + val notificationManager = context.getSystemService( + NotificationManager::class.java + ) + if (notificationManager != null) { + val notification = ongoingCallNotification + notificationManager.notify(callNotificationId, notification) + } + } + + val ongoingCallNotification: Notification + get() { + val hangupIntent = Intent( + context, + CallActionReceiver::class.java + ) + hangupIntent.action = CallActionReceiver.ACTION_END_CALL + val hangupPendingIntent = PendingIntent.getBroadcast( + context, + CallActionReceiver.ACTION_END_CALL_ID, + hangupIntent, + PendingIntent.FLAG_IMMUTABLE + ) + val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Notification.Builder(context, NotificationChannelManager.ONGOING_CALL_CHANNEL_ID) + } else { + Notification.Builder(context) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setColorized(true) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val caller = Person.Builder() + .setName(remoteName) + .setImportant(true) + .build() + builder.style = Notification.CallStyle.forOngoingCall(caller, hangupPendingIntent) + } else { + builder.addAction( + Notification.Action.Builder( + R.drawable.end_call, "End call", hangupPendingIntent + ).build() + ) + } + + builder.setColor(-0xde690d) + builder.setOngoing(true) + .setContentTitle("Ongoing call") + .setContentText("Talking with $remoteName...") + .setSmallIcon(R.drawable.ic_stat_ic_notification) + .setOnlyAlertOnce(true) + .setUsesChronometer(true) + .setWhen(System.currentTimeMillis()) + + return builder.build() + } + + companion object { + private val TAG: String = VonageConnection::class.java.simpleName + } +} diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnectionHolder.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnectionHolder.kt new file mode 100644 index 00000000..52e03e65 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnectionHolder.kt @@ -0,0 +1,8 @@ +package com.vonage.basic_video_chat_connectionservice.connectionservice + +import android.annotation.SuppressLint + +object VonageConnectionHolder { + @SuppressLint("StaticFieldLeak") + var connection: VonageConnection? = null +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnectionService.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnectionService.kt new file mode 100644 index 00000000..c8fa7692 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/connectionservice/VonageConnectionService.kt @@ -0,0 +1,184 @@ +package com.vonage.basic_video_chat_connectionservice.connectionservice + +import android.app.Notification +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.telecom.Connection +import android.telecom.ConnectionRequest +import android.telecom.ConnectionService +import android.telecom.PhoneAccountHandle +import android.telecom.TelecomManager +import android.util.Log +import com.vonage.basic_video_chat_connectionservice.Call +import com.vonage.basic_video_chat_connectionservice.CallActionReceiver +import com.vonage.basic_video_chat_connectionservice.CallHolder +import com.vonage.basic_video_chat_connectionservice.CallState +import com.vonage.basic_video_chat_connectionservice.VonageManager +import com.vonage.basic_video_chat_connectionservice.deviceselector.AudioDeviceSelector +import dagger.hilt.android.AndroidEntryPoint +import java.util.Random +import javax.inject.Inject + +@AndroidEntryPoint +class VonageConnectionService : ConnectionService() { + + @Inject + lateinit var vonageManager: VonageManager + @Inject + lateinit var audioDeviceSelector: AudioDeviceSelector + @Inject + lateinit var callHolder: CallHolder + + override fun onCreateOutgoingConnection( + connectionManagerPhoneAccount: PhoneAccountHandle, + request: ConnectionRequest + ): Connection { + val addressUri = request.address + val callerName = addressUri.getQueryParameter("callerName") ?: "" + val random = Random() + val randomValue = random.nextInt() * random.nextInt() + + val call = Call( + callID = randomValue, + name = callerName, + state = CallState.CONNECTING + ) + callHolder.setCall(call) + + val connection = VonageConnection( + context = applicationContext, + audioDeviceSelector = audioDeviceSelector, + vonageManager = vonageManager, + remoteName = callerName, + callNotificationId = randomValue, + callHolder = callHolder + ) + + connection.onCallEnded = { + connection.onCallEnded = null + onCallEnded() + } + + connection.setInitialized() + + connection.setCallerDisplayName(callerName, TelecomManager.PRESENTATION_ALLOWED) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + connection.connectionProperties = Connection.PROPERTY_SELF_MANAGED + } + + connection.audioModeIsVoip = true + connection.videoState = request.videoState + + val capabilities = + Connection.CAPABILITY_HOLD or Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_MUTE + connection.connectionCapabilities = capabilities + + audioDeviceSelector.listener = connection + + VonageConnectionHolder.connection = connection + + val notification: Notification = connection.ongoingCallNotification + startForeground(randomValue, notification) + + connection.onPlaceCall() + + return connection + } + + override fun onCreateIncomingConnection( + connectionManagerPhoneAccount: PhoneAccountHandle, + request: ConnectionRequest + ): Connection { + val extras = request.extras + val callerName = extras.getString(PhoneAccountManager.CALLER_NAME) ?: "" + + val random = Random() + val randomValue = random.nextInt() * random.nextInt() + + val call = Call( + callID = randomValue, + name = callerName, + state = CallState.CONNECTING + ) + callHolder.setCall(call) + + val connection = VonageConnection( + context = applicationContext, + audioDeviceSelector = audioDeviceSelector, + vonageManager = vonageManager, + remoteName = callerName, + callNotificationId = randomValue, + callHolder = callHolder + ) + connection.onCallEnded = { + connection.onCallEnded = null + onCallEnded() + } + connection.setRinging() + connection.setCallerDisplayName(callerName, TelecomManager.PRESENTATION_ALLOWED) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + connection.connectionProperties = Connection.PROPERTY_SELF_MANAGED + } + + connection.audioModeIsVoip = true + connection.videoState = request.videoState + + val capabilities = + Connection.CAPABILITY_HOLD or Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_MUTE + connection.connectionCapabilities = capabilities + + audioDeviceSelector.listener = connection + + VonageConnectionHolder.connection = connection + + val notification = connection.getIncomingCallNotification(true) + startForeground(randomValue, notification) + + return connection + } + + override fun onCreateIncomingConnectionFailed( + connectionManagerPhoneAccount: PhoneAccountHandle, + request: ConnectionRequest + ) { + Log.e(TAG, "Incoming connection failed: " + request.address) + VonageConnectionHolder.connection = null + } + + override fun onCreateOutgoingConnectionFailed( + connectionManagerPhoneAccount: PhoneAccountHandle, + request: ConnectionRequest + ) { + Log.e(TAG, "Outgoing connection failed: " + request.address) + VonageConnectionHolder.connection = null + } + + override fun onConnectionServiceFocusGained() { + super.onConnectionServiceFocusGained() + + vonageManager.notifyAudioFocusIsActive() + Log.d(TAG, "onConnectionServiceFocusGained") + } + + override fun onConnectionServiceFocusLost() { + super.onConnectionServiceFocusLost() + + vonageManager.notifyAudioFocusIsInactive() + Log.d(TAG, "onConnectionServiceFocusLost") + } + + private fun onCallEnded() { + stopForeground(true) + VonageConnectionHolder.connection = null + callHolder.setCall(null) + } + + companion object { + private val TAG: String = VonageConnectionService::class.java.simpleName + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelectionListener.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelectionListener.kt new file mode 100644 index 00000000..5c72641a --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelectionListener.kt @@ -0,0 +1,9 @@ +package com.vonage.basic_video_chat_connectionservice.deviceselector + +interface AudioDeviceSelectionListener { + /** + * Is called when the user selects an audio device. + * @param device The selected audio device. + */ + fun onAudioDeviceSelected(device: AudioDeviceSelector.AudioDevice) +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelector.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelector.kt new file mode 100644 index 00000000..613f884c --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelector.kt @@ -0,0 +1,149 @@ +package com.vonage.basic_video_chat_connectionservice.deviceselector + +import android.os.Build +import android.telecom.CallAudioState +import android.telecom.CallEndpoint +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + + +class AudioDeviceSelector { + private val TAG: String = AudioDeviceSelector::class.java.simpleName + + class AudioDevice { + val name: String + val type: Int + val endpoint: CallEndpoint? + + internal constructor(name: String, type: Int) { + this.name = name + this.type = type + this.endpoint = null + } + + internal constructor(name: String, type: Int, endpoint: CallEndpoint?) { + this.name = name + this.type = type + this.endpoint = endpoint + } + } + + private val _availableDevices = MutableStateFlow>(emptyList()) + val availableDevices: StateFlow> = _availableDevices.asStateFlow() + + private val _activeDevice = MutableStateFlow(null) + val activeDevice: StateFlow = _activeDevice.asStateFlow() + + var listener: AudioDeviceSelectionListener? = null + + fun onAvailableCallEndpointsChanged(endpoints: List) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return + } + + val devices: MutableList = ArrayList() + + for (endpoint in endpoints) { + val name: String + val type: Int + + when (endpoint.endpointType) { + CallEndpoint.TYPE_BLUETOOTH -> { + name = "Bluetooth" + type = CallAudioState.ROUTE_BLUETOOTH + } + + CallEndpoint.TYPE_WIRED_HEADSET -> { + name = "Wired Headset" + type = CallAudioState.ROUTE_WIRED_HEADSET + } + + CallEndpoint.TYPE_SPEAKER -> { + name = "Speaker" + type = CallAudioState.ROUTE_SPEAKER + } + + CallEndpoint.TYPE_EARPIECE -> { + name = "Earpiece" + type = CallAudioState.ROUTE_EARPIECE + } + + else -> { + name = "Unknown" + type = 0 + } + } + + devices.add(AudioDevice(name, type, endpoint)) + } + + _availableDevices.value = devices + } + + fun onCallEndpointChanged(endpoint: CallEndpoint) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return + } + + val devices = availableDevices.value ?: return + + for (device in devices) { + if (device.endpoint != null && + device.endpoint.endpointType == endpoint.endpointType + ) { + _activeDevice.value = device + break + } + } + } + + fun onCallAudioStateChanged(audioState: CallAudioState) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + // In Android 14+, use CallEndpoint for audio routing + return + } + + val devices: MutableList = ArrayList() + val supportedRoutes = audioState.supportedRouteMask + val currentRoute = audioState.route + + if ((supportedRoutes and CallAudioState.ROUTE_EARPIECE) != 0) { + val device = AudioDevice("Earpiece", CallAudioState.ROUTE_EARPIECE) + devices.add(device) + if (currentRoute == CallAudioState.ROUTE_EARPIECE) { + _activeDevice.value = device + } + } + + if ((supportedRoutes and CallAudioState.ROUTE_BLUETOOTH) != 0) { + val device = AudioDevice("Bluetooth", CallAudioState.ROUTE_BLUETOOTH) + devices.add(device) + if (currentRoute == CallAudioState.ROUTE_BLUETOOTH) { + _activeDevice.value = device + } + } + + if ((supportedRoutes and CallAudioState.ROUTE_WIRED_HEADSET) != 0) { + val device = AudioDevice("Wired Headset", CallAudioState.ROUTE_WIRED_HEADSET) + devices.add(device) + if (currentRoute == CallAudioState.ROUTE_WIRED_HEADSET) { + _activeDevice.value = device + } + } + + if ((supportedRoutes and CallAudioState.ROUTE_SPEAKER) != 0) { + val device = AudioDevice("Speaker", CallAudioState.ROUTE_SPEAKER) + devices.add(device) + if (currentRoute == CallAudioState.ROUTE_SPEAKER) { + _activeDevice.value = device + } + } + + _availableDevices.value = devices + } + + fun selectDevice(device: AudioDevice) { + listener?.onAudioDeviceSelected(device) + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelectorDialog.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelectorDialog.kt new file mode 100644 index 00000000..80c4699c --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/deviceselector/AudioDeviceSelectorDialog.kt @@ -0,0 +1,67 @@ +package com.vonage.basic_video_chat_connectionservice.deviceselector + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun AudioDeviceSelectorDialog( + audioDeviceSelector: AudioDeviceSelector, + onDismissRequest: () -> Unit +) { + val availableDevices by audioDeviceSelector.availableDevices.collectAsState() + val activeDevice by audioDeviceSelector.activeDevice.collectAsState() + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text("Audio devices") }, + text = { + LazyColumn { + items(availableDevices) { device -> + val isSelected = device == activeDevice + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + audioDeviceSelector.selectDevice(device) + onDismissRequest() + } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = isSelected, + onClick = { + audioDeviceSelector.selectDevice(device) + onDismissRequest() + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = device.name) + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text("Close") + } + } + ) +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/di/AppModule.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/di/AppModule.kt new file mode 100644 index 00000000..c269d10d --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/di/AppModule.kt @@ -0,0 +1,89 @@ +package com.vonage.basic_video_chat_connectionservice.di + +import android.content.Context +import android.content.Context.POWER_SERVICE +import android.os.PowerManager +import android.telecom.TelecomManager +import com.opentok.android.AudioDeviceManager +import com.vonage.basic_video_chat_connectionservice.CallHolder +import com.vonage.basic_video_chat_connectionservice.VonageManager +import com.vonage.basic_video_chat_connectionservice.connectionservice.PhoneAccountManager +import com.vonage.basic_video_chat_connectionservice.deviceselector.AudioDeviceSelector +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import com.vonage.basic_video_chat_connectionservice.NotificationChannelManager + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + @Provides + @Singleton + fun provideVonageManager( + @ApplicationContext context: Context, + audioDeviceManager: AudioDeviceManager, + callHolder: CallHolder + ): VonageManager { + val manager = VonageManager(context, audioDeviceManager, callHolder) + manager.setAudioFocusManager() + return manager + } + + @Provides + @Singleton + fun provideAudioDeviceManager( + @ApplicationContext context: Context + ): AudioDeviceManager { + return AudioDeviceManager(context) + } + + @Provides + @Singleton + fun provideAudioDeviceSelector( + ): AudioDeviceSelector { + return AudioDeviceSelector() + } + + @Provides + @Singleton + fun providePhoneAccountManager( + @ApplicationContext context: Context, + telecomManager: TelecomManager + ): PhoneAccountManager { + return PhoneAccountManager(context, telecomManager) + } + + @Provides + @Singleton + fun provideTelecomManager( + @ApplicationContext context: Context + ): TelecomManager { + return context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + } + + @Provides + @Singleton + fun providePowerManager( + @ApplicationContext context: Context + ): PowerManager { + return context.getSystemService(POWER_SERVICE) as PowerManager + } + + @Provides + @Singleton + fun provideNotificationChannelManager( + @ApplicationContext context: Context + ): NotificationChannelManager { + return NotificationChannelManager(context) + } + + @Provides + @Singleton + fun provideCallHolder( + ): CallHolder { + return CallHolder() + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/home/HomeView.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/home/HomeView.kt new file mode 100644 index 00000000..ac62615f --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/home/HomeView.kt @@ -0,0 +1,48 @@ +package com.vonage.basic_video_chat_connectionservice.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun HomeView(homeViewModel: HomeViewModel, modifier: Modifier) { + HomeView( + onOutgoingCall = homeViewModel::startOutgoingCall, + onIncomingCall = homeViewModel::startIncomingCall, + modifier = modifier) +} + +@Composable +fun HomeView( + onOutgoingCall: () -> Unit = {}, + onIncomingCall: () -> Unit = {}, + modifier: Modifier +) { + + Column(modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center) { + Button(onClick = { + onIncomingCall() + }) { + Text(text = "Incoming Call") + } + Button(onClick = { + onOutgoingCall() + }) { + Text(text = "Outgoing Call") + } + } +} + +@Preview +@Composable +fun HomeViewPreview() { + HomeView(modifier = Modifier) +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/home/HomeViewModel.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/home/HomeViewModel.kt new file mode 100644 index 00000000..fe209a61 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/home/HomeViewModel.kt @@ -0,0 +1,28 @@ +package com.vonage.basic_video_chat_connectionservice.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vonage.basic_video_chat_connectionservice.usecases.StartIncomingCallUseCase +import com.vonage.basic_video_chat_connectionservice.usecases.StartOutgoingCallUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val startIncomingCallUseCase: StartIncomingCallUseCase, + private val startOutgoingCallUseCase: StartOutgoingCallUseCase +): ViewModel() { + + fun startOutgoingCall() { + viewModelScope.launch { + startOutgoingCallUseCase() + } + } + + fun startIncomingCall() { + viewModelScope.launch { + startIncomingCallUseCase() + } + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/PublisherView.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/PublisherView.kt new file mode 100644 index 00000000..ae4fc2f1 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/PublisherView.kt @@ -0,0 +1,26 @@ +package com.vonage.basic_video_chat_connectionservice.room + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView + +@Composable +fun PublisherView( + androidView: android.view.View?, + modifier: Modifier = Modifier +) { + AndroidView( + factory = { context -> + android.widget.FrameLayout(context) + }, + update = { container -> + container.removeAllViews() + + androidView?.let { view -> + (view.parent as? android.view.ViewGroup)?.removeView(view) + container.addView(view) + } + }, + modifier = modifier + ) +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/RoomView.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/RoomView.kt new file mode 100644 index 00000000..7a9e66fd --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/RoomView.kt @@ -0,0 +1,176 @@ +package com.vonage.basic_video_chat_connectionservice.room + +import android.view.View +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun RoomView(roomViewModel: RoomViewModel, onShowAudioDevicesClick: () -> Unit) { + val callStatus by roomViewModel.callStatusName.collectAsState() + val callName by roomViewModel.callName.collectAsState() + val isOnHold by roomViewModel.isOnHold.collectAsState() + val publisherView by roomViewModel.publisherViewFlow.collectAsState() + val subscriberViews by roomViewModel.subscriberViewsFlow.collectAsState() + + RoomView( + callStatus = callStatus, + participantName = callName, + isPublisherVisible = true, + isOnHoldVisible = isOnHold, + publisherView = publisherView, + subscriberViews = subscriberViews, + onShowAudioDevicesClick = onShowAudioDevicesClick, + onHangUpClick = roomViewModel::onEndCall, + onUnHoldClick = roomViewModel::onUnhold + ) +} + +@Composable +fun RoomView( + callStatus: String, + participantName: String, + isPublisherVisible: Boolean, + isOnHoldVisible: Boolean, + publisherView: View?? = null, + subscriberViews: List = emptyList(), + onShowAudioDevicesClick: () -> Unit, + onHangUpClick: () -> Unit, + onUnHoldClick: () -> Unit +) { + val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + val navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + Column( + modifier = Modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color(0x80000000)) + .padding(top = statusBarHeight + 8.dp, bottom = 8.dp), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if (callStatus.isNotEmpty()) { + Text( + text = callStatus, + color = Color.White, + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + } + if (participantName.isNotEmpty()) { + Text( + text = participantName, + color = Color.White, + fontSize = 16.sp + ) + } + } + + Button( + onClick = onShowAudioDevicesClick, + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE91E63)), + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 10.dp) + ) { + Text("Audio") + } + } + + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + SubscriberGrid(subscriberViews) + + if (isPublisherVisible && publisherView != null) { + Box( + modifier = Modifier + .size(width = 90.dp, height = 120.dp) + .align(Alignment.BottomEnd) + .padding(end = 16.dp, bottom = 16.dp) + .background(Color(0xFFCCCCCC)) + ) { + PublisherView( + androidView = publisherView, + modifier = Modifier.fillMaxSize() + ) + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color(0x80000000)) + .padding( + vertical = 8.dp, + horizontal = 16.dp + ).padding( + bottom = navBarHeight + 8.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onHangUpClick, + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE91E63)) + ) { + Text("HangUp") + } + + if (isOnHoldVisible) { + Spacer(modifier = Modifier.width(8.dp)) + Button(onClick = onUnHoldClick) { + Text("Unhold call") + } + } + + Spacer(modifier = Modifier.weight(1f)) + } + } +} + +@Preview +@Composable +fun RoomViewPreview() { + RoomView( + callStatus = "In call", + participantName = "John Doe", + isPublisherVisible = true, + isOnHoldVisible = false, + publisherView = null, + subscriberViews = emptyList(), + onShowAudioDevicesClick = {}, + onHangUpClick = {}, + onUnHoldClick = {} + ) +} + diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/RoomViewModel.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/RoomViewModel.kt new file mode 100644 index 00000000..1896692d --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/RoomViewModel.kt @@ -0,0 +1,76 @@ +package com.vonage.basic_video_chat_connectionservice.room + +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vonage.basic_video_chat_connectionservice.CallHolder +import com.vonage.basic_video_chat_connectionservice.CallState +import com.vonage.basic_video_chat_connectionservice.usecases.EndCallUseCase +import com.vonage.basic_video_chat_connectionservice.usecases.UnholdUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class RoomViewModel @Inject constructor( +private val callHolder: CallHolder, +private val endCallUseCase: EndCallUseCase, +private val unholdUseCase: UnholdUseCase +): ViewModel() { + + val publisherViewFlow = callHolder.publisherViewFlow.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + null + ) + + val subscriberViewsFlow = callHolder.subscriberViewFlow.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptyList() + ) + + val callStatusName = callHolder.callStateFlow + .map { state -> + when (state) { + CallState.IDLE -> "Idle" + CallState.CONNECTING -> "Connecting" + CallState.CONNECTED -> "Connected" + CallState.DIALING -> "Dialing" + CallState.ANSWERING -> "Answering" + CallState.HOLDING -> "Holding" + CallState.DISCONNECTED -> "Disconnected" + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + "Idle" + ) + + val callName = callHolder.callFlow + .map { call -> + call?.name ?: "No Call" + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + "No Call" + ) + + val isOnHold = callHolder.callStateFlow + .map { it == CallState.HOLDING } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + false + ) + + fun onEndCall() { + endCallUseCase() + } + + fun onUnhold() { + unholdUseCase() + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/SubscriberGrid.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/SubscriberGrid.kt new file mode 100644 index 00000000..7365bca5 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/room/SubscriberGrid.kt @@ -0,0 +1,59 @@ +package com.vonage.basic_video_chat_connectionservice.room + +import android.view.View +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun SubscriberGrid( + subscriberViews: List, + modifier: Modifier = Modifier +) { + BoxWithConstraints(modifier = modifier.fillMaxSize()) { + val screenWidth = maxWidth + val screenHeight = maxHeight + + val columns = when { + subscriberViews.isEmpty() -> 1 + subscriberViews.size == 1 -> 1 + subscriberViews.size <= 4 -> 2 + subscriberViews.size <= 9 -> 3 + else -> 4 + } + + val cellWidth = screenWidth / columns + val cellHeight = cellWidth * 4/3 + + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = Modifier.padding(4.dp) + ) { + items(subscriberViews.size) { index -> + val subscriberView = subscriberViews[index] + Box( + modifier = Modifier + .width(cellWidth - 8.dp) + .height(cellHeight - 8.dp) + .padding(4.dp) + .background(Color(0xFFCCCCCC)) + ) { + PublisherView( + androidView = subscriberView, + modifier = Modifier.fillMaxSize() + ) + } + } + } + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Color.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Color.kt new file mode 100644 index 00000000..e43611d5 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.vonage.basic_video_chat_connectionservice.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Theme.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Theme.kt new file mode 100644 index 00000000..2bc863b7 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.vonage.basic_video_chat_connectionservice.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun BasicVideoChatConnectionServiceKotlinTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Type.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Type.kt new file mode 100644 index 00000000..5f8a56cc --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.vonage.basic_video_chat_connectionservice.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/EndCallUseCase.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/EndCallUseCase.kt new file mode 100644 index 00000000..b902bd44 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/EndCallUseCase.kt @@ -0,0 +1,11 @@ +package com.vonage.basic_video_chat_connectionservice.usecases + +import com.vonage.basic_video_chat_connectionservice.connectionservice.VonageConnectionHolder +import javax.inject.Inject + +class EndCallUseCase @Inject constructor() { + + operator fun invoke() { + VonageConnectionHolder.connection?.onDisconnect() + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/StartIncomingCallUseCase.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/StartIncomingCallUseCase.kt new file mode 100644 index 00000000..ce1ad5a6 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/StartIncomingCallUseCase.kt @@ -0,0 +1,18 @@ +package com.vonage.basic_video_chat_connectionservice.usecases + +import com.vonage.basic_video_chat_connectionservice.connectionservice.PhoneAccountManager +import javax.inject.Inject + +class StartIncomingCallUseCase @Inject constructor( +private val phoneAccountManager: PhoneAccountManager +) { + + operator fun invoke() { + val callerName = "Simulated Caller" + val callerId = "+4401539702257" + + if (phoneAccountManager.canPlaceIncomingCall()) { + phoneAccountManager.notifyIncomingVideoCall(callerName, callerId) + } + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/StartOutgoingCallUseCase.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/StartOutgoingCallUseCase.kt new file mode 100644 index 00000000..19417a18 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/StartOutgoingCallUseCase.kt @@ -0,0 +1,18 @@ +package com.vonage.basic_video_chat_connectionservice.usecases + +import com.vonage.basic_video_chat_connectionservice.connectionservice.PhoneAccountManager +import javax.inject.Inject + +class StartOutgoingCallUseCase @Inject constructor( +private val phoneAccountManager: PhoneAccountManager +) { + + operator fun invoke() { + val callerName = "Simulated Caller" + val callerId = "+4401539702257" + + if (phoneAccountManager.canPlaceOutgoingCall()) { + phoneAccountManager.startOutgoingVideoCall(callerName, callerId) + } + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/UnholdUseCase.kt b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/UnholdUseCase.kt new file mode 100644 index 00000000..f89e5400 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/java/com/vonage/basic_video_chat_connectionservice/usecases/UnholdUseCase.kt @@ -0,0 +1,11 @@ +package com.vonage.basic_video_chat_connectionservice.usecases + +import com.vonage.basic_video_chat_connectionservice.connectionservice.VonageConnectionHolder +import javax.inject.Inject + +class UnholdUseCase @Inject constructor() { + + operator fun invoke() { + VonageConnectionHolder.connection?.onUnhold() + } +} \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/answer_call.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/answer_call.xml new file mode 100644 index 00000000..d16e9854 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/answer_call.xml @@ -0,0 +1,9 @@ + + + diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/end_call.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/end_call.xml new file mode 100644 index 00000000..d0a7d883 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/end_call.xml @@ -0,0 +1,9 @@ + + + diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_launcher_background.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_launcher_foreground.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_stat_ic_notification.png b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/drawable/ic_stat_ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..54a85c91ae121b24f751fee1b96825bdbddce3af GIT binary patch literal 855 zcmV-d1E~CoP)E>1S`}F(1Z&fwI zH@@&2=Z7^wM}^lUiYC#_%*Fx1eq0V-0Vo!W zF+#{~A;cez$);((u`KHzW9*2IS(cSdBocG_xmYZ&cN}LxDa98S7S1h~as{ALsca2` z;ARj6zcc`d2$WLboPQ44w*3MCj!;TBAmT;F*cL>53IO}l>GbR7Dpmk$wb~xW*ql;o zT`QfnY_VAEfl_LtPA;VcLdbi^aSkSv$wzGg)oL})7@N@z)?eQ0P6+v&CkTSZJkitB zb3BzwU2h90l}e+W^V8kBqbHbs$$5c1MA&A&34%(GBHtybGx zuh-|Kl(8rkAmXCqIEP%h~3 zgpjvGLqk7>0`mF%Sw!qpN=1dd1%TOHE;kYi@H~%FN_P-Kjwz+~BI1TFSM-9E@-C%x zc4=wpKhN_-D4-G4E1Gk@ODWZdh&%Pp+Cnycz&t?2S<^Hhc6H!1H#ncq{{R4gY7hWy z0|5GUTyF)Bbz%Uxo6F@sv@%9le|BSIV?QgU4oNBZA)*3+TZUoW8Xg{g*U52R0XijV hH@*hwsNVN4(I<7Xyf&508esqc002ovPDHLkV1m+UlG*?O literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..5bd089622f16160076f6c3a601a6f0f8069d3499 GIT binary patch literal 818 zcmV-21I_$WNk&F00{{S5MM6+kP&iB;0{{RoN5Byfb;!Z?&yb}5;B4Er71_3}-1c2) z+h%Cnw$0tWnYFs6gZ^*#JXOhr|E`&-j)0nN+p^_*<-^^rl6(V;97Hmm75++cN$!&V zpBZcbUFVl`0x0yZ2R#dE2SJY{sAI_?(l_*7{a8QLkGNLSCMd|U;E+C|zXyaF{=47Nl5unwD3@3a+Bt4kXGo@_?Xmu%Mpq}Ez78H6m1tDoq6jsmh`V4|Oi2id% z5IfWByRbRPF&Cn+x_Wl+Scth4=h=gmzgOZseGJ{CXsZZgX(t6|gJNu>qj=D4-~x;t zWe7%+27u9J0}T)Izmkx=t_j9*IUpt33>R`1H-pP?K~Nn4V{|5llQ#-8$h_XOc2{-? zy~c|i0dbi@Sy5vFbOeT*%kneGVq#eNA`c|L%lJ_pA#yp5IW-qRqlzDSLQY66vCREe z9^xFwaOBeh;O}Np}(-;B#S^-k7$KWe@Aa+Cq%??0T^#$hF06=%p zh#-B+19N1;4$29M_%K3f7AouwiKc4<7+n_8h#|k4EDd%=PDnxDFoMWr5+4MhlQ6<$ z`I;9}z++Qu0*lb^G7RxP6GsC^$d5etlZjW zSY0Cm1?mY7>_C1{n$ZI!nEd>mDZ=UkM%2SUn6dR>hK?o}=(+Oj&nI0b?lbt^|L?jE z1B)^g=G%0@6fCcpF literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..5d62354336a2d69b4beade3130acb1c080c78ea1 GIT binary patch literal 670 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 literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..a7d29d6c1fe877ab56712a67074294ca1d95f4d9 GIT binary patch literal 438 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 W=NJn4?3h7P!`J^`|9}1ee_Q~rfS;HE literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..94c27d0c5f98021397160abf27223321699124c2 GIT binary patch literal 2922 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 U%uSM1p*6U6-%-1*brD{U0l$mPQUCw| literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c279caae5c309dc7cf242326fb7e1ebc16d13b51 GIT binary patch literal 1592 zcmV-82FLkQNk&F61^@t8MM6+kP&iB@1^@srkH8}km4<@0jhr)o*f)Lxh=>XB`RI>m zoKaX48!e_)ZSqT6w5=VIq%R`dwr$(CZQHhO+qP}nw#F*kl^OrjRNp@$*7_gE->4bU z{|TUjZJTL3dt=*XnJ!hTq_=JLsqB=tQ#YN4Z%*8${K-FC3DG2nck(%bNcwhH38;dYL!q@De^Nnk6fiQ!(tY?c@i39 zl%Gmg=_-x*Bn^x)VP3>0RO4z--Kr1ur+$ey#p>1xLRfJg#74EMbA`1M4y|?nYBRC6 z*yM(+mA0^jD?A31Ah3cDqp)I4p-HWke?FVy&wl5Aj|sIXVa^(Pp-6m2i~VQUWBrY* zBZT-dsgSnMs65;XkNAdV;Q7P0$Rdfe5^wC0TbCdJzQ+r8mVWrUkv#nVE@$)88ui@dUqr!Fb%IEj%E=Mo+u8g%gJi|9k|;+-ajh8o_M{$l&=p z#1&k&aZ5#TJO=?C-)6yLpS8`bcUb5VVvNAJv0Xy{A-v!a!Vgkmv7g#x`#Y>Y=MYd5 z*&fsSx@-|qj_~9cr0|7!5GVf878e~@;s|I7s)g}%mkq*O5gvSk7~bo^vU%FzX*}%H zMg-J&6~}m;b)6?1XW<2ulJg6F!uPB);RoQrxrTWE?Czg@7ibGRA}du;8!e zgl9R3;(gJv%x^5Xl^iR>W&~7)<-_z@<{1MBkG??`FNp?m9xv0rOpldjAp*KQGGNiy zn>I_wnvfC3<1~nig`4D;jo@e!0?MAH!eSjXX?=98K9)3B7mRBdrbLz`d^jm-d?_&& z=Y}b#Vq(=ffPl8JdYIPUlm>)% zyh_7TdrJM76#I9jfWGf@Vti4?m`V6Q?-(#9V);U3ymLxq4g>r}3TTCp-AH)q3OI=D zg?tQ<@v9(VA{O{wthZ^SgzSkJSiuN*2y6JZL~mq5n-HFUhY3EZN2RqFvNsZA@lFI> zgcXe{{$&|wZ^GM>vcYThD5^VUo@c;HvIYSkf$4r0Yp;-BtvF@Y%Ixb#W)+Ld&ZLQY7pM! zonrXd$agZ=;$g{gp_(v%mhGTn?Fm+n{1@>Fj`_QTe}?c0LgW?b?$ zSZyT!^56OjX)`plS;usptu2+G{M`Q<*N}}dMb}$pT@rphMZ)DJi7vji%gQ?FqJwPb zkI0V)lAObfbbfH~5K+aD8U_a<^2Ewt2#1v%(s|6E?^uUSW#$_Pab@{yRjkg&8uK*29XLb@{@8oL5ygGWS)hk+j{UpK!8ZZAP(^n5=f6Cix`8@5I#^4 qu=HE&!MN~800JEZk?uHQ>{<`Pn!L0YGV&OVB5O?^TL#TPl>z|r>=hIM literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..0e54418551eadc75d9a33af89ce62add2d5c907d GIT binary patch literal 1342 zcmV-E1;P4KNk&FC1pok7MM6+kP&iB}1pojqL%~oGpx8)~BS|v-)n0P#)s4w~+7S{U zNiuD#_uTtz+qP|f+qP}nwr$(CZQJ(0-ktz>{r~#^_5bVt*Z;5oU;n@U{~Ne-cs+yQ z=P@5c=RP}i0yOJ!xcFPB^Vv~(7Wl*YX$->9GwRO{^a#+Phhgq*p$B~U?EDD4WAQAS zVdw|@PY+BEP|Ig=y)Xqne*W}sc?1S)DqOe?cmE4jWX)u&@jUD~1ewVs)Na zrhpVX8E_y#?a#9mLkD5B$_fuDr}3aERG1CnR)DIXW+aC05VHZT3a1WZ1hGwp4KR9{zhV*<31kRJ>9Ygpeq%4hreo+d3}4;fD2_PzRj4&RYR`Uh zgNXFO&=acZfgV3;1}!r$3teFR?ELu7Tjr6582ZX~eqi!<20}}?R2UBn$e;d;Kb#|Z z7}E0D0I>dxIS|`XSOqI66%iSQM#2~aQo;^^Q;4=hY#xS=z!Iv7C@NM+${`-EmaLezc{UlbA_p}r3x{m;j<==78byk z%EpjR;F>s6SPEOJ5JOr)TS7Qnm=9a328RCe*%D^%7bd`#>Vly!9JhpvFNH3!r50f5 z4)vDEU<^n_u%yh~#LybpQo}IxnfsPVrYkgrC1vA2hBm^MT8W_-oVG+~3~6P+k}9PO zNUdQ>CHaY=39zJMeZtTpVo7B9f}wd7*ijAr!|NFhCSXO`T0r9UKBx#QO3}beyq>{A zF0i2-wRZ=v_sc>Nu%Ob6aFiA=rt!*g(^O*(um4~Fzy5#y|N8&+|LgzP|9|%f0NNaS AtN;K2 literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..bd7c93b26eb2b64f96e191a0a4876dfd49e01c92 GIT binary patch literal 4556 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 literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..80581d45c44cf59f9183f7d44d2668a9e053d31a GIT binary patch literal 2176 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 CjXui& literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..0ac60b591de4d7a4d65049b9ef0ac1ae1646ae39 GIT binary patch literal 1842 zcmV-22hI3WNk&F02LJ$9MM6+kP&iB;2LJ#sufb~&@GVJ_B}tNP;s5`3Udo+Z9fGy` zVi6K(+cxcH-}k=bq_%C_wr$%DYTLGL+qN0o_RoLs?g@a$|BwG4|3ChJ{Qvm>@&Duh z$N!K2AOAo8fBgUW|Nm? zRDTMx6+V~^SoD`V1u(IIR-TKiL8ko2F&m3w*+v1D6ws2>FbYao z4y#l^RzWQ5Vf4qM9ED~94iwOY!+4W$2zIFoz2Gn9C>mptuG%ob?E>m?6c-Zi!7{Z= zczzOPP!J;3OX@F4$bp9ONr6r7pZ8D24a43BYbFnCfWgK8(0c|@4 zqmYEfuul187Nqk$gn?L;CI39Y-U9MH1L0l5KG>&9w1Pn~4xU7f`1I*p5YCiHU$=2@gMyhqQzduu*2=1-jZFXFe81GLHoK z6wt{}V-l9&2P@?%qrk5ChZ%xJnY?BKb{5da&mw$C*abUPj-w!j+ec}EMVbmj0aprW z?nk+ra2=Mac|s`D2dTi3&~hq-cLg-`gY3ehkHl2KpoGuF$MBSyFa);BD71jud<-8f zieMZIn456tQN^=zCDDn|nW06DI z!GH?|wE835NVp7()tD?u$zlL4;_X!-}(i$!l?v-%|jGUViwkuYF1jDiZN zf6hWI3T7M)n3eFZ#w;pfHmsJb)B?Nfu>?gJ1ojm zYB=C*0qsq>op278s{vV%r9enkdP0Ng5MC6}(vSmK^pcnk=$#NwWE7B{&@kh zcEtt+PQJ@%$hx0!3Kpym`5w{C8P(|pQk@BbC-1QcvJPX>6WFlsZ;>f~MqxP#-A6?A z_Z{{^)(R~8OC1rI^ai?$8CLNLlVHVc5?<#9WKG1P1hyH0m9Nt(L-jjhCG1!cIj{1G z$mohiIf@Jk9DS9cko6?t2rO9*@-@PkGU|{8QuQf;2d^<7vQA*peb}PVlrf1O}Gf# z)&>zJaLTAaE0CIui|`rID#+T6MXzDqhNB!sGV;kt=sPcFk&Z&vLM#ekm=`F=J;N*} zVH#XSW3VWZ^F^Tgm9P>nqV8CfrO-|ADq#;?L@meyhq5=pT`W2c7g0U3Kv(@HxQ0bn z;UcO{E6~-w39ew#O}L2akp)t{o8T@MU4@INC0U@Uc@YS2u;?^gM18TyuG~dn;-9b+ zE}}VDl%l{zP*8G$H(W%Auqc4#B4|uc=mGbT(N`?G1s9Pw7G)`W5mc0w-~;!N>J=7! zX1oY|u_#v!xQFV?P4I(z$c;Z1J!7~B6c@25UNN|b{IJNOBiuv1XbIkM4>gpDMMsEx zpqd0My2Aw5P-RhA^prDPLv_Vr(Id`q4Rw`s$|KtD1|BwG4|3ChJ{Qv(20(RS6c>n+a literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1600939ae364073f8ac6c0a7af9560ad0d2816f0 GIT binary patch literal 6560 zcmV;R8DHj7Nk&GP82|uRMM6+kP&iDB82|t;zrZgL^@f7Bjhuu(?3+ITM8pJe*tPFF zzE4b8C<|s}%?r2>xB(QL6C@8bJ5$x2b<5dFeANEGPUGnN52UzF?|o)_-}K&l@4ffl zdt)}e_uiAK5v}TP&7(LX{f=!IxM;WlVtq;YNmF& z$=V{@MH&$gg`rH;6U{)|(RuU$eL(+E5voG9h(a_1Zp)N@^S?K5uF`L~!Smnln?xGz za*J+J?6OD_v5^V2L9@^y^akZ*Vigi#BBnEQOOfmUQEOz}Ad-~rEjpvVg{o^u-O&0f zRETM8BPfDcm4u{{VxXR)RU&9i<8@Vq8{1pxAF}WvQ}u|85^g}(Q68pfCF-m}CGA#1 zW4hfzeK|pBe+mAt{RVLl?c)~q{}gtUD6U3vXfAq;sxe7Kh&oA4trn$0WgRVrsvE8= zT}}Joh1;9-p)fQG-KLaGD^rfG6hS0PXkCrMD0_=gdz(-?+PQ_^R|!rSEGG!Yc*~xj znuOT;_xnT`cYl261MH|K`sIy5l$-+=A=rXw~ zrmc9iAP8wlvIsyVS4dNcYA~)LpalO|j`8({UQx93T~%~PKQUIow)m8Dg2yMDiludlJCB&iE zOe){S#Fg=+i+?ud091x++N^?LupAR*u><+$vWv(UEoV{%Ar{B4i1rihyox1K;uOY+ z2q&yoJuY5(daPWu^C)8VDHst%Sp~&lF@d<}GYxzhCc7B#K|eI=!TV!taO}_7(fqhV zO|qDffpRgbEmlH!Bx-SNZcIZyoNH{wYfy=}nGaU`UnGFk|DEH9tjFkt62R=80%ErdL*OL9iVayabb zf1qz_x(TzH0*ihOGm2adSzIh3p3*|_W8RC)6&yVxTf_iyoXQHHHL;j|8vMi-E)Em} z$|X3n&8h?7&Rl`1s{^8miw=t37-w2_0+m$p5{w@buyYVAqEInL#Hu4m5X1bEA&IOH zSsX7Uo~tzp`@A^JcpWVb$n2U-0mVjI=#H@r6Ltnnta=b55Z8ghq$@o0F|1c$HJe&s zfsXitYpgnju6NW*zmiU|8)#&0@D0_J=?(xOZm7m1%(i z3^r;4F2!k0JI?u9LKf~-VoFGim`A|iGx{?42TUyFT`Sv?HJaf-GBXGV!)e**1H*Dp zP(WH|qjyi2to>^}5Y16M(EQ4|-zs*Bq&`OaD!;yu5%di-_-q^V?5%=8i?%3qt?EP0 zn&LzV37PdrDfuLzgs{|DWzZ3rgFYi&(M+x9OZdtc+B|n#g8x#X^NK#?tT{FW!0@z)(8mW~xfnCUZu^y^T!mWBQEbIStsY!zO^i1y&@sj# zhPzox)XTP!FCHt?mi=9nWB9=G-uS3(6$;bAg*MX_c-Baj_NkBDN%kRq0a3moMMdXf`21m!)uPgyE zOs?(KP@5*Vm}A#z{kgVYT<}6Wl!k3yLEnXyS@8n&-EF=_i~R6F`2IB z>?ffS*dIu#4kF0U$Nc|VgzKvejbUCk0Ashd*clbW(MS>>20VmrQ(T22FbkKCH*0Ec zaRDRYA{(Erc9xi>rl6Ni3|}jiCxGokZUP5i+HFSrTPh#b39a>e(SnRytSU6KaZ|^QvZkaK-Y+*<WUeLgwKv;rU(`Gfc8%G8Rz6&c^FhWbmU_`sEY;c+r@Oo=1ShAD_`=fm=qz>M;{lYn+LXGyoj5|YKI;`Nn zbQsoJh-I}m@U!=4A%;kMzw5wptwY-V!<2v60#5|>Mqsgn>sbMF_^pD<;PNQ%r)=h0 zP4=_fVH{)$dY^;cV@0C*mXt-TAc3jq)-RNz{>dBV56tC@So})yWi}*a@%IU&54gD! zRzk^~hf6j(ZZ@fZG6FmH8rHK0)4MPnOc2MOco;vG5_ZiY$eMl+fNzfSSDOi4qNg&f zv9`qQ262qHi{W;jxOt$xBfKARgw!T#fibyZCU&!jHC`}W%n>G8Qb~+kHDS2%Wa_j| zz9^;|1Zsh~U>vL8C^=Rb%m02V`I;cUzW_?N$pV8`=9aVPJ8A=JZk_x4H!t5#W?1CX zYr%rc89~1IPZ)_l5ePwka_Q0L{B>#&x|bmKF1t|CL+@c#2ycMi+7K4W3iN*VXVQ=w z-0V9wQ1J&_+5L)_eTUgUivMC_SZObU z&B4J>{Gs4!#4+VxYV7z0v-8_*U`^|Yv65!_(>Czr3G=D|y8a3JwL)>@O=k>D{}cy& zn9)3EG-mrJs(1Luuzj-t$r1%@msQ28=C&=}=QOLqYgpbz%dy1v7wsB>(l6mb#oYWq z7_!!}SP(T2=WO#PGUHAR;4M2yI$7q%t3WqPDcjPUVQFu!qk=)C?y_^nUoqk-e!j+4 zS4k(^EFv-cSyLLYE&XqPQnmL9vcGZo#VcY=!-wNsI)V za8{xB+k=(BWNdYF`ujBTA2`5~44`@~qm1P~F>Li0zYZfF1HU~*pU#9p6f^xR_2PeX z0G@71LgxBCMbtTi5pkz@3`76F-kF{lU;?nCZHgQ&a2Sl8XBz4$iMHOsjIlF|$2uzi zJ#o!ZY$kk;}CM0}qsH8U^DjKc2~qUjz5Nc%q6EaAS*ti^yYafF;9aOaQ|8>W?)7?i)BvD6n0a zMAUMFmEU8hQ5*b~(@2bLCrKDr_vxm#C@uqB1-5gOh#GF7RfGs=3NzU`hLc%L!noRx zH?{rD#eiQMdnOW<+<@18iUgkLUW3X1CPvEtXc$-Zp(e&RaG3B*!|YS#e`eK$ z=X*>AqUPWSJCeeTFh5HO(vjmZL30Kz=PB^H!Z5-X-Ci=1jJ9d`?);pmh|G+53Lhj_ zcE!a?nzJ$ed7~6L4pS8r$kFyv;l>l_&kX~~V+G83C=bNnN>P+>n7GF1G>(m@amnHX zeC4x*w;>EGeOt@=`|}y$x(5eDFEqsdpcQg;cN$iYX6bH%kF%EO^3o^hko)|5q*H$< zt}7riv^s$WLQpThi4}x9T=Syg)zZW8OZ)BM>fSmUSr0+hoc|Pp@{VDB+FS`TP6bg zzhN>IblVhsYf;vnL_QLX7Z1qKXhGBz2f2e@1$}~Vf|&&7d#+|MT@M~ndt%SJy9q-9 zW@P8Q!XEpbOCx{lP|9WugDx-4auZ!!D!lsdljC#lB~SzN5uAF^F1*C0k&Sb{=?s@F zYruTZ+C5Czv*E1!5L|y4*UStTu>MR6>T!!WP4GC!FW|>?6^j^Aj!^bAoV3NcE}iVf zTzGFudyg(e9hV8#s{ewQ7QBJ;6MaZ;q8m*^a z6C?N?gh)wO{Cz}139XXZxTlj00%x7TK`RfrD^avsoQnj=vT|!245Iva>zmPacr@F*}{1OP6bpPoKyB22(kb@A3!)L4&+lAbqU~{peVRQ!&4`2LgceRP{7czvkkfcZ$h4Tt@2(G(qr3n*4!l zWkm4_S`GBhjw4J-=0tpFZ~n$gY7aG!S3=<18!aj3)pj1Jxa@RgN~FR*r7vz*aI8vY z5A3{;_>LOTk2)zU>kRzKrw4tKPe{K?`3yu~_2i^8RO<|#z&|FS>dW1i68=nT4|Ijj zqqN>IfO^-Es-xA7K9xNXIy*~22NC0L$$GwE1GuotN>yVfjrnuX^$8facanKS0S!T# z|4<8I-y77!^nkS9}P@Hx-Znbyak+e3pIg7hkGa4_(sG%yXnk)$LB z?(vHsY_nR@nAp!m^uxq7$w;|1pPK6r*LgnHBc_H2udn41&9KLr_Azb0+`HyYgFJ@? z);Fup=nHpL+&-kN&#^T@uBRQQ=nVfhCT22Dx>UBVqk`;A13Z|V8eZ0DN{-C!>q{Ny z4|`-YkHH?aitD2Mr?qJyXH_@+!oUalQDI={wbEakN5~&dJYZV=o)-Tx)9zC{0-xWq z>&WoBMtWzh(jeMj+pv#g#Ea$N?)CE8khoZx?An*!QkmEjb#u`5eHTx|emXss*A8S8 zkXE!p!LxzdVbOub2~a8>PFhVHU~%*d2i5_=$^OL+Eq zyXIl5feUP&JZvanH&0w*s3Oa?CjI#9N3uKX{Y-a`R!x8l(tU%V-eK+`@0NnRd~^s6 z1euMlUk-`R-6uH5M!R#go`4AgP#&QNsyv1o?Mp}rG=~`yKRRe^g{6_ zvoc7HR>PVEy8Vq{J!$bI6e=m55OP0!a!t{{Z)echl0X2;+lNFN!PaB{rUcq8eAK#! z;%g@?J8vJFRjHAtOh1Ci-&vo%kw7VE;ddW{lkS0&x-nJWi!4eukYpTSNIW6- zvgfnWD0C2HB2IsMTw>yFpmEzY)07I}B+wmZ1j~anulf=Lfe(UIb^UR_#?^tV2^NpX~WeO)A4UX-m952a8bo4w?w$XUXh#DuGjQb zjxJDr2?To-=DiLV-=@<6jA+{)aZE9;>WpjuM{)}|s=QtrGWqq~fRsFiZ zxOM@y2KgC?k``kyPJ8u|L6+(Rohen$oW^uo@Mw@H)`P0R)6tq-6cPE^H5X==*$E=? z@DPHmLfKi_A6*|2U6lNLPeFS$8bRWRAWx*_Kvz+O`(=GkyV<99Dw6nPFp!SD z({WYpcW=LVKsdr#A$rpMZs0Tb;F-&&C&iR~?H!y&;Wv-^!J|HPld+pz82GW0r=fS< z$&X(@CR|iqnnJ32n~IH>3m++hssO?xP07^}t_aVYN1b}-%BrzgidwR@f$QFg-ZlYO zRDJ*Mf9<%YD!H_w092&vQ(f^@%mbOT$rEWl&{=H>zqH@C&s>;49CIN{l9D!Xrnn`^ zx&R|_LBZG0zNCNHQeCmu)8tIbQ&IUl46fkD`V!DWNN%KsT0FY-=Qnr%)SsG!r652M zQZR6#5Cm`A)Sq(}H%~N%Ev3m~ZhmVPRi&cxeGvkt7X)rlfeu1) zvy%^SM>s9v`!{?4yzgJ%dFx~>f{Z2FF$OKYv_EeN8`ez6wHJ85cm2c#_iYj3wnR8n zle2THwr7E)((4G(Rst9*i>y`%20a)$*eg0XE#dG3T13tI*7+V^`&5j=3@pK3DisV; zr3U8TK?98nIvAwo?6WZz{VIBX{nq*S&Urw~2v6A3!C8^L%hS}Vx}ri*ixOuVgh524 zxr3vW5{^lI$)HcH`~J0zD@w25_@kvLEXHk_nG(sM3MG=6nQa*FwXdIy5xBav*VoX; z*S~o1(P?2)Ded5hw4|h_IZK#V!df)yll-8o^6ZMVbZ{Ktt|0#c?>Hj$IlXLOK5)wI zo}Vwx?_22`k4-=NN&B`~$TZQgw{Z8G4fBw^@y7!yFU#-s`T7jovwIsihb=`Fsp8sQ zo~rE-kfd*9S_y3d3}6Engrpq;NdZh9>=mg+7E3t1`Ou`Fy>w{Q+)r-!>ap)%JM(dm zcfGz|@cXiVS7cw6*E_#Y{?042ulRS-?{mKPeA{@^)vq1>1GRS=#F#z<=i`##RniaWX}YdE}P^Y8Xe`s0Ro zZ~lO`4{87Cj@Oxg>-kOP?>6rUhbeD%Md_{%_CQ%~L)CU?Ru0V%eyS@$3NF$PEJ`SW z0MJ4ZnF0ftA}x`&4$gg?hejS5S<%IPN0&tnvr{T=sM=NS62SxpGMCosFQJrl((hta zgleY*;N{gPmH0tZ&;+3kpofTEW!Yekv_{%GIK+J%$)zNdlSR=LNX#zVmEvj0Zz{;} zmn7Ba8A*l7uL}f6v(1>N&jERs-`QG_)>M@bVGSOSCzmAIsUbDLswu6Y)#=Zw%hBf< zZOsm5MK=hvRJzyJ2m*b*JE^WD3sio;)9K8}Aboy+dsazZlDl4C*=X~o({c=o?ht4x znzh;MwHX^5EA#U7H0I@1HZ~etIn&t SQdB!S%_LnU>HT<_69E8j^}VnF literal 0 HcmV?d00001 diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/colors.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/ic_launcher_background.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..c5d5899f --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/strings.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..a16fa23c --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Basic Video Chat ConnectionService Kotlin + \ No newline at end of file diff --git a/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/themes.xml b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..bb54a3b6 --- /dev/null +++ b/Basic-Video-Chat-ConnectionService-Kotlin/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +