From 824eca10d20491ca5756740dca70340d64f8c343 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 19 Jul 2024 17:33:41 +0200 Subject: [PATCH 1/4] Add initial structure for Android tutorial --- .../openvidu-android/.gitignore | 15 + .../openvidu-android/.idea/.gitignore | 3 + .../openvidu-android/.idea/.name | 1 + .../openvidu-android/.idea/compiler.xml | 6 + .../.idea/deploymentTargetSelector.xml | 18 ++ .../openvidu-android/.idea/gradle.xml | 19 ++ .../openvidu-android/.idea/kotlinc.xml | 6 + .../openvidu-android/.idea/migrations.xml | 10 + .../openvidu-android/.idea/misc.xml | 10 + .../openvidu-android/.idea/other.xml | 263 ++++++++++++++++++ .../openvidu-android/.idea/vcs.xml | 6 + .../openvidu-android/app/.gitignore | 1 + .../openvidu-android/app/build.gradle.kts | 59 ++++ .../openvidu-android/app/proguard-rules.pro | 21 ++ .../android/ExampleInstrumentedTest.kt | 24 ++ .../app/src/main/AndroidManifest.xml | 39 +++ .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 25073 bytes .../java/io/openvidu/android/MainActivity.kt | 81 ++++++ .../io/openvidu/android/RoomLayoutActivity.kt | 181 ++++++++++++ .../res/drawable/ic_launcher_background.xml | 170 +++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ .../app/src/main/res/drawable/ic_settings.xml | 12 + .../app/src/main/res/layout/activity_main.xml | 109 ++++++++ .../main/res/layout/activity_room_layout.xml | 19 ++ .../src/main/res/layout/dialog_settings.xml | 26 ++ .../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 -> 1298 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 1406 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2730 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1010 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 892 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1762 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1716 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 1986 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3738 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2540 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 3140 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5986 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3598 bytes .../ic_launcher_foreground.webp | Bin 0 -> 4584 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 8350 bytes .../app/src/main/res/values-night/themes.xml | 8 + .../app/src/main/res/values/colors.xml | 9 + .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 11 + .../app/src/main/res/values/themes.xml | 12 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../io/openvidu/android/ExampleUnitTest.kt | 17 ++ .../openvidu-android/build.gradle.kts | 5 + .../openvidu-android/gradle.properties | 23 ++ .../gradle/libs.versions.toml | 40 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + application-client/openvidu-android/gradlew | 185 ++++++++++++ .../openvidu-android/gradlew.bat | 89 ++++++ .../openvidu-android/settings.gradle.kts | 24 ++ 58 files changed, 1604 insertions(+) create mode 100644 application-client/openvidu-android/.gitignore create mode 100644 application-client/openvidu-android/.idea/.gitignore create mode 100644 application-client/openvidu-android/.idea/.name create mode 100644 application-client/openvidu-android/.idea/compiler.xml create mode 100644 application-client/openvidu-android/.idea/deploymentTargetSelector.xml create mode 100644 application-client/openvidu-android/.idea/gradle.xml create mode 100644 application-client/openvidu-android/.idea/kotlinc.xml create mode 100644 application-client/openvidu-android/.idea/migrations.xml create mode 100644 application-client/openvidu-android/.idea/misc.xml create mode 100644 application-client/openvidu-android/.idea/other.xml create mode 100644 application-client/openvidu-android/.idea/vcs.xml create mode 100644 application-client/openvidu-android/app/.gitignore create mode 100644 application-client/openvidu-android/app/build.gradle.kts create mode 100644 application-client/openvidu-android/app/proguard-rules.pro create mode 100644 application-client/openvidu-android/app/src/androidTest/java/io/openvidu/android/ExampleInstrumentedTest.kt create mode 100644 application-client/openvidu-android/app/src/main/AndroidManifest.xml create mode 100644 application-client/openvidu-android/app/src/main/ic_launcher-playstore.png create mode 100644 application-client/openvidu-android/app/src/main/java/io/openvidu/android/MainActivity.kt create mode 100644 application-client/openvidu-android/app/src/main/java/io/openvidu/android/RoomLayoutActivity.kt create mode 100644 application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 application-client/openvidu-android/app/src/main/res/drawable/ic_settings.xml create mode 100644 application-client/openvidu-android/app/src/main/res/layout/activity_main.xml create mode 100644 application-client/openvidu-android/app/src/main/res/layout/activity_room_layout.xml create mode 100644 application-client/openvidu-android/app/src/main/res/layout/dialog_settings.xml create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 application-client/openvidu-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 application-client/openvidu-android/app/src/main/res/values-night/themes.xml create mode 100644 application-client/openvidu-android/app/src/main/res/values/colors.xml create mode 100644 application-client/openvidu-android/app/src/main/res/values/ic_launcher_background.xml create mode 100644 application-client/openvidu-android/app/src/main/res/values/strings.xml create mode 100644 application-client/openvidu-android/app/src/main/res/values/themes.xml create mode 100644 application-client/openvidu-android/app/src/main/res/xml/backup_rules.xml create mode 100644 application-client/openvidu-android/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 application-client/openvidu-android/app/src/test/java/io/openvidu/android/ExampleUnitTest.kt create mode 100644 application-client/openvidu-android/build.gradle.kts create mode 100644 application-client/openvidu-android/gradle.properties create mode 100644 application-client/openvidu-android/gradle/libs.versions.toml create mode 100644 application-client/openvidu-android/gradle/wrapper/gradle-wrapper.jar create mode 100644 application-client/openvidu-android/gradle/wrapper/gradle-wrapper.properties create mode 100644 application-client/openvidu-android/gradlew create mode 100644 application-client/openvidu-android/gradlew.bat create mode 100644 application-client/openvidu-android/settings.gradle.kts diff --git a/application-client/openvidu-android/.gitignore b/application-client/openvidu-android/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/application-client/openvidu-android/.gitignore @@ -0,0 +1,15 @@ +*.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 diff --git a/application-client/openvidu-android/.idea/.gitignore b/application-client/openvidu-android/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/application-client/openvidu-android/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/application-client/openvidu-android/.idea/.name b/application-client/openvidu-android/.idea/.name new file mode 100644 index 00000000..a4b2ad72 --- /dev/null +++ b/application-client/openvidu-android/.idea/.name @@ -0,0 +1 @@ +Basic Android \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/compiler.xml b/application-client/openvidu-android/.idea/compiler.xml new file mode 100644 index 00000000..b589d56e --- /dev/null +++ b/application-client/openvidu-android/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/deploymentTargetSelector.xml b/application-client/openvidu-android/.idea/deploymentTargetSelector.xml new file mode 100644 index 00000000..bae10838 --- /dev/null +++ b/application-client/openvidu-android/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/gradle.xml b/application-client/openvidu-android/.idea/gradle.xml new file mode 100644 index 00000000..0897082f --- /dev/null +++ b/application-client/openvidu-android/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/kotlinc.xml b/application-client/openvidu-android/.idea/kotlinc.xml new file mode 100644 index 00000000..fdf8d994 --- /dev/null +++ b/application-client/openvidu-android/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/migrations.xml b/application-client/openvidu-android/.idea/migrations.xml new file mode 100644 index 00000000..f8051a6f --- /dev/null +++ b/application-client/openvidu-android/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/misc.xml b/application-client/openvidu-android/.idea/misc.xml new file mode 100644 index 00000000..0ad17cbd --- /dev/null +++ b/application-client/openvidu-android/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/other.xml b/application-client/openvidu-android/.idea/other.xml new file mode 100644 index 00000000..0d3a1fbb --- /dev/null +++ b/application-client/openvidu-android/.idea/other.xml @@ -0,0 +1,263 @@ + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/.idea/vcs.xml b/application-client/openvidu-android/.idea/vcs.xml new file mode 100644 index 00000000..b2bdec2d --- /dev/null +++ b/application-client/openvidu-android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/app/.gitignore b/application-client/openvidu-android/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/application-client/openvidu-android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/application-client/openvidu-android/app/build.gradle.kts b/application-client/openvidu-android/app/build.gradle.kts new file mode 100644 index 00000000..237e65f5 --- /dev/null +++ b/application-client/openvidu-android/app/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.jetbrains.kotlin.plugin.serialization) +} + +android { + namespace = "io.openvidu.android" + compileSdk = 34 + + defaultConfig { + applicationId = "io.openvidu.android" + minSdk = 24 + targetSdk = 34 + 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_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(libs.livekit.android) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.annotation) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/application-client/openvidu-android/app/proguard-rules.pro b/application-client/openvidu-android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/application-client/openvidu-android/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/application-client/openvidu-android/app/src/androidTest/java/io/openvidu/android/ExampleInstrumentedTest.kt b/application-client/openvidu-android/app/src/androidTest/java/io/openvidu/android/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..a91e9916 --- /dev/null +++ b/application-client/openvidu-android/app/src/androidTest/java/io/openvidu/android/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package io.openvidu.android + +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("io.openvidu.android", appContext.packageName) + } +} \ No newline at end of file diff --git a/application-client/openvidu-android/app/src/main/AndroidManifest.xml b/application-client/openvidu-android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..5eab0157 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/app/src/main/ic_launcher-playstore.png b/application-client/openvidu-android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..68e0e28c45cd4d1d70e81150915233cc979bb1ec GIT binary patch literal 25073 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

$*gC1?;>s<;A_WZeO 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 z rXX4$&w3~yI>|pY+Mf;^yo4VCCt&*0fKMOeGIA2x^)TgivtuT>uw-%8T}IW zLeIVJYc;mPdO)=Cqbo(i9c{f&ksoDtI*WT_6+3 zF>f+Hqp2i3lq0Wr;L7sFt*qL6j%UA1Piud?GiGgEoo$F8+7_>*iD($VCyYierE@~F zN&Ulkr*8P_Hw*(y=@-+bsO&$M z_Ftsnv>)a$Fn~Cb-hd9#)>0N&FvwyX@_+)s_YaTH{d{DR$muSwaYh{*asf}MfMGZ6 ztRC>=N`rC1L^yXsDobCy$KOZ*XsdHGU^T7FGixVmf$u)+PDJcMe-~9g%|Bp_>E+R0B7uxYpdjR-X4#1t;rnj&+k&*ucAs9$A literal 0 HcmV?d00001 diff --git a/application-client/openvidu-android/app/src/main/java/io/openvidu/android/MainActivity.kt b/application-client/openvidu-android/app/src/main/java/io/openvidu/android/MainActivity.kt new file mode 100644 index 00000000..51fa1a97 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/java/io/openvidu/android/MainActivity.kt @@ -0,0 +1,81 @@ +package io.openvidu.android + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.widget.Button +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.floatingactionbutton.FloatingActionButton + +class MainActivity : AppCompatActivity() { + private lateinit var participantField: EditText + private lateinit var roomField: EditText + private lateinit var joinButton: Button + + private var applicationServerUrl = "https://192-168-1-136.openvidu-local.dev:6443/" + private var livekitUrl = "wss://192-168-1-136.openvidu-local.dev:7443/" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + participantField = findViewById(R.id.participantName) + roomField = findViewById(R.id.roomName) + joinButton = findViewById(R.id.joinButton) + val settingsButton = findViewById(R.id.settingsButton) + + participantField.setText("Participant %d".format((1..100).random())) + + joinButton.setOnClickListener { + navigateToRoomLayoutActivity() + } + + settingsButton.setOnClickListener { + showSettingsDialog() + } + } + + private fun navigateToRoomLayoutActivity() { + joinButton.isEnabled = false + + val participantName = participantField.text.toString() + val roomName = roomField.text.toString() + + if (participantName.isNotEmpty() && roomName.isNotEmpty()) { + val intent = Intent(this, RoomLayoutActivity::class.java) + intent.putExtra("participantName", participantName) + intent.putExtra("roomName", roomName) + intent.putExtra("serverUrl", applicationServerUrl) + intent.putExtra("livekitUrl", livekitUrl) + startActivity(intent) + } + + joinButton.isEnabled = true + } + + private fun showSettingsDialog() { + val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_settings, null) + val serverUrl = dialogView.findViewById(R.id.serverUrl) + val liveKitUrl = dialogView.findViewById(R.id.livekitUrl) + + serverUrl.setText(applicationServerUrl) + liveKitUrl.setText(livekitUrl) + + val builder = AlertDialog.Builder(this) + builder.setTitle("Configure URLs") + .setView(dialogView) + .setPositiveButton("Save") { dialog, _ -> + applicationServerUrl = serverUrl.text.toString() + livekitUrl = liveKitUrl.text.toString() + dialog.dismiss() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + + val dialog = builder.create() + dialog.show() + } +} \ No newline at end of file diff --git a/application-client/openvidu-android/app/src/main/java/io/openvidu/android/RoomLayoutActivity.kt b/application-client/openvidu-android/app/src/main/java/io/openvidu/android/RoomLayoutActivity.kt new file mode 100644 index 00000000..ab08c0f2 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/java/io/openvidu/android/RoomLayoutActivity.kt @@ -0,0 +1,181 @@ +package io.openvidu.android + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import io.livekit.android.LiveKit +import io.livekit.android.events.RoomEvent +import io.livekit.android.events.collect +import io.livekit.android.room.Room +import io.livekit.android.renderer.SurfaceViewRenderer +import io.livekit.android.room.track.VideoTrack +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +class RoomLayoutActivity : AppCompatActivity() { + private lateinit var APPLICATION_SERVER_URL: String + private lateinit var LIVEKIT_URL: String + + private lateinit var room: Room + + private val client = HttpClient(CIO) { + expectSuccess = true + install(ContentNegotiation) { + json() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_room_layout) + + APPLICATION_SERVER_URL = intent.getStringExtra("serverUrl") ?: "" + LIVEKIT_URL = intent.getStringExtra("livekitUrl") ?: "" + + // Create Room object. + room = LiveKit.create(applicationContext) + + // Setup the video renderer + room.initVideoRenderer(findViewById(R.id.renderer)) + + requestNeededPermissions { connectToRoom() } + } + + private fun connectToRoom() { + val participantName = intent.getStringExtra("participantName") ?: "Participant 1" + val roomName = intent.getStringExtra("roomName") ?: "Test Room" + + lifecycleScope.launch { + // Setup event handling. + launch { + room.events.collect { event -> + when (event) { + is RoomEvent.TrackSubscribed -> onTrackSubscribed(event) + is RoomEvent.TrackUnsubscribed -> onTrackUnsubscribed(event) + else -> {} + } + } + } + + try { + // Get token from server. + val token = getToken(roomName, participantName) + + // Connect to server. + room.connect(LIVEKIT_URL, token) + + // Turn on audio/video recording. + val localParticipant = room.localParticipant + localParticipant.setMicrophoneEnabled(true) + localParticipant.setCameraEnabled(true) + } catch (e: Exception) { + println("There was an error connecting to the room: ${e.message}") + Toast.makeText(this@RoomLayoutActivity, "Failed to join room", Toast.LENGTH_SHORT) + .show() + leaveRoom() + } + } + } + + private fun onTrackSubscribed(event: RoomEvent.TrackSubscribed) { + val track = event.track + + if (track is VideoTrack) { + attachVideo(track) + } + } + + private fun attachVideo(videoTrack: VideoTrack) { + videoTrack.addRenderer(findViewById(R.id.renderer)) + findViewById(R.id.progress).visibility = View.GONE + } + + private fun onTrackUnsubscribed(event: RoomEvent.TrackUnsubscribed) { + val track = event.track + + if (track is VideoTrack) { + detachVideo(track) + } + } + + private fun detachVideo(videoTrack: VideoTrack) { + videoTrack.removeRenderer(findViewById(R.id.renderer)) + findViewById(R.id.progress).visibility = View.VISIBLE + } + + private fun leaveRoom() { + room.disconnect() + client.close() + // Go back to the previous activity. + finish() + } + + override fun onDestroy() { + super.onDestroy() + leaveRoom() + } + + private fun requestNeededPermissions(onHasPermissions: () -> Unit) { + val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants -> + var hasDenied = false + + // Check if any permissions weren't granted. + for (grant in grants.entries) { + if (!grant.value) { + Toast.makeText(this, "Missing permission: ${grant.key}", Toast.LENGTH_SHORT) + .show() + + hasDenied = true + } + } + + if (!hasDenied) { + onHasPermissions() + } + } + + // Assemble the needed permissions to request + val neededPermissions = + listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA).filter { + ContextCompat.checkSelfPermission( + this, it + ) == PackageManager.PERMISSION_DENIED + }.toTypedArray() + + if (neededPermissions.isNotEmpty()) { + requestPermissionLauncher.launch(neededPermissions) + } else { + onHasPermissions() + } + } + + private suspend fun getToken(roomName: String, participantName: String): String { + val response = client.post(APPLICATION_SERVER_URL + "token") { + contentType(ContentType.Application.Json) + setBody(TokenRequest(participantName, roomName)) + } + return response.body().token + } +} + +@Serializable +data class TokenRequest(val participantName: String, val roomName: String) + +@Serializable +data class TokenResponse(val token: String) diff --git a/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml b/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_foreground.xml b/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/application-client/openvidu-android/app/src/main/res/drawable/ic_settings.xml b/application-client/openvidu-android/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000..6abc6b78 --- /dev/null +++ b/application-client/openvidu-android/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/application-client/openvidu-android/app/src/main/res/layout/activity_main.xml b/application-client/openvidu-android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..10880abb --- /dev/null +++ b/application-client/openvidu-android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + +