diff --git a/openvidu-androidx/.gitignore b/openvidu-androidx/.gitignore
new file mode 100644
index 000000000..603b14077
--- /dev/null
+++ b/openvidu-androidx/.gitignore
@@ -0,0 +1,14 @@
+*.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
diff --git a/openvidu-androidx/.idea/.name b/openvidu-androidx/.idea/.name
new file mode 100644
index 000000000..f2ca64933
--- /dev/null
+++ b/openvidu-androidx/.idea/.name
@@ -0,0 +1 @@
+OpenVidu Android
\ No newline at end of file
diff --git a/openvidu-androidx/.idea/codeStyles/Project.xml b/openvidu-androidx/.idea/codeStyles/Project.xml
new file mode 100644
index 000000000..681f41ae2
--- /dev/null
+++ b/openvidu-androidx/.idea/codeStyles/Project.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-androidx/.idea/codeStyles/codeStyleConfig.xml b/openvidu-androidx/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 000000000..a55e7a179
--- /dev/null
+++ b/openvidu-androidx/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-androidx/.idea/gradle.xml b/openvidu-androidx/.idea/gradle.xml
new file mode 100644
index 000000000..5cd135a06
--- /dev/null
+++ b/openvidu-androidx/.idea/gradle.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-androidx/.idea/jarRepositories.xml b/openvidu-androidx/.idea/jarRepositories.xml
new file mode 100644
index 000000000..a5f05cd8c
--- /dev/null
+++ b/openvidu-androidx/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-androidx/.idea/misc.xml b/openvidu-androidx/.idea/misc.xml
new file mode 100644
index 000000000..b6ea2b11a
--- /dev/null
+++ b/openvidu-androidx/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-androidx/.idea/runConfigurations.xml b/openvidu-androidx/.idea/runConfigurations.xml
new file mode 100644
index 000000000..7f68460d8
--- /dev/null
+++ b/openvidu-androidx/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-androidx/.idea/vcs.xml b/openvidu-androidx/.idea/vcs.xml
new file mode 100644
index 000000000..6c0b86358
--- /dev/null
+++ b/openvidu-androidx/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-androidx/app/.gitignore b/openvidu-androidx/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/openvidu-androidx/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/openvidu-androidx/app/build.gradle b/openvidu-androidx/app/build.gradle
new file mode 100644
index 000000000..052099c90
--- /dev/null
+++ b/openvidu-androidx/app/build.gradle
@@ -0,0 +1,45 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.1"
+ defaultConfig {
+ applicationId "io.openvidu.openvidu_android"
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ multiDexEnabled true
+ }
+ buildFeatures {
+ viewBinding true
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ debuggable true
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+ implementation 'com.squareup.okhttp3:okhttp:4.2.0'
+ implementation 'com.neovisionaries:nv-websocket-client:2.9'
+ implementation 'org.webrtc:google-webrtc:1.0.30039'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test:runner:1.2.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+}
diff --git a/openvidu-androidx/app/proguard-rules.pro b/openvidu-androidx/app/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/openvidu-androidx/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
diff --git a/openvidu-androidx/app/src/androidTest/java/io/openvidu/openvidu_android/ExampleInstrumentedTest.java b/openvidu-androidx/app/src/androidTest/java/io/openvidu/openvidu_android/ExampleInstrumentedTest.java
new file mode 100644
index 000000000..247ac721a
--- /dev/null
+++ b/openvidu-androidx/app/src/androidTest/java/io/openvidu/openvidu_android/ExampleInstrumentedTest.java
@@ -0,0 +1,27 @@
+package io.openvidu.openvidu_android;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ assertEquals("io.openvidu.openvidu_android", appContext.getPackageName());
+ }
+}
diff --git a/openvidu-androidx/app/src/main/AndroidManifest.xml b/openvidu-androidx/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..b8bc14dcc
--- /dev/null
+++ b/openvidu-androidx/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-androidx/app/src/main/ic_launcher-web.png b/openvidu-androidx/app/src/main/ic_launcher-web.png
new file mode 100644
index 000000000..08fc776e6
Binary files /dev/null and b/openvidu-androidx/app/src/main/ic_launcher-web.png differ
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/activities/SessionActivity.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/activities/SessionActivity.java
new file mode 100644
index 000000000..84c45b632
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/activities/SessionActivity.java
@@ -0,0 +1,582 @@
+package io.openvidu.openvidu_android.activities;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Editable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.DialogFragment;
+
+import org.jetbrains.annotations.NotNull;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.EglBase;
+import org.webrtc.MediaStream;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoTrack;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Random;
+
+import io.openvidu.openvidu_android.R;
+import io.openvidu.openvidu_android.databinding.ActivityMainBinding;
+import io.openvidu.openvidu_android.fragments.PermissionsDialogFragment;
+import io.openvidu.openvidu_android.openvidu.LocalParticipant;
+import io.openvidu.openvidu_android.openvidu.RemoteParticipant;
+import io.openvidu.openvidu_android.openvidu.Session;
+import io.openvidu.openvidu_android.utils.CustomHttpClient;
+import io.openvidu.openvidu_android.utils.OpenViduUtils;
+import io.openvidu.openvidu_android.websocket.CustomWebSocket;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+/**
+ *
+ */
+public class SessionActivity extends AppCompatActivity {
+
+ /** Android */
+ private static final int MY_PERMISSIONS_REQUEST_CAMERA = 100;
+ private static final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 101;
+ private static final int MY_PERMISSIONS_REQUEST = 102;
+
+ private SharedPreferences sharedPreferences = null; // initialized in onCreate
+
+ /** Logging */
+ private final String TAG = "SessionActivity";
+
+ /** Layout */
+ // View Binding for layout activity_main.xml; (initialized in onCreate)
+ private ActivityMainBinding activityMainBinding = null;
+
+ /** HTTP */
+ static final String METHOD_POST = "POST";
+
+ /** OpenVidu */
+ static final String CONFIG_URL = "/config";
+ static final String SESSION_URL = "/api/sessions";
+ static final String TOKEN_URL = "/api/tokens";
+
+ private String OPENVIDU_URL;
+ private String OPENVIDU_SECRET; // WARNING: For example only; use login and user tokens in production
+ private Session session;
+ private CustomHttpClient httpClient;
+ private static boolean bAuthenticated;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "LIFECYCLE: onCreate");
+ super.onCreate(savedInstanceState);
+
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+ loadSharedPreferences();
+
+ // Initialize View Binding for activity_main and set content View
+ // The class name "ActivityMainBinding" is automatically generated from the layout filename activity_main
+ // https://developer.android.com/topic/libraries/view-binding
+ activityMainBinding = ActivityMainBinding.inflate(getLayoutInflater());
+ View rootView = activityMainBinding.getRoot();
+ setContentView( rootView );
+
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+
+ askForPermissions();
+
+ generateRandomName();
+ }
+
+ private void loadSharedPreferences(){
+ final String sharedPreferencesName = getString(R.string.main_shared_preferences);
+ sharedPreferences = getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
+ }
+
+ /**
+ *
+ */
+ @Override
+ protected void onStart() {
+ Log.i(TAG, "LIFECYCLE: onStart");
+ super.onStart();
+ }
+
+ /**
+ * Restore state
+ */
+ @Override
+ protected void onResume() {
+ Log.i(TAG, "LIFECYCLE: onResume");
+ super.onResume();
+
+ if( bAuthenticated ) {
+ viewToLoggedInState();
+ initHttpClientUnsecure();
+ }else{
+ viewToLoggedOutState();
+ }
+
+ String urlString = sharedPreferences.getString(getString(R.string.sp_key_openvidu_url), getString(R.string.default_openvidu_url));
+ activityMainBinding.openviduUrl.setText(urlString);
+ }
+
+ /**
+ * Save state
+ */
+ @Override
+ protected void onPause() {
+ Log.i(TAG, "LIFECYCLE: onPause");
+ super.onPause();
+
+ SharedPreferences.Editor spEditor = sharedPreferences.edit();
+ spEditor.putString(getSharedPrefKey(R.string.sp_openvidu_url), activityMainBinding.openviduUrl.getText().toString());
+ spEditor.apply();
+ }
+
+ /**
+ *
+ */
+ @Override
+ protected void onStop() {
+ Log.i(TAG, "LIFECYCLE: onStop");
+ leaveSession();
+ super.onStop();
+ }
+
+ /**
+ *
+ */
+ @Override
+ protected void onDestroy() {
+ Log.i(TAG, "LIFECYCLE: onDestroy");
+ leaveSession();
+ super.onDestroy();
+ }
+
+
+ /**
+ * Ask for permissions CAMERA and/or RECORD_AUDIO
+ */
+ public void askForPermissions() {
+ boolean bCameraPermission
+ = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
+ boolean bRecordAudioPermission
+ = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
+
+ if( !bCameraPermission && !bRecordAudioPermission ) {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO}, MY_PERMISSIONS_REQUEST);
+ } else if ( !bRecordAudioPermission ){
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.RECORD_AUDIO}, MY_PERMISSIONS_REQUEST_RECORD_AUDIO);
+ } else if ( !bCameraPermission ) {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.CAMERA}, MY_PERMISSIONS_REQUEST_CAMERA);
+ }
+ }
+
+ /**
+ * This is a pseudo-login that performs an API request to see if it was successful.
+ *
+ * @param view
+ */
+ public void onClickLogin(View view) {
+ initHttpClientUnsecure();
+ verifyOpenViduAuthentication();
+ }
+
+ /**
+ * Join or leave a session depending on the text value of id/start_finish_call.
+ *
+ * @param view
+ */
+ public void onClickStartFinishCall(View view) {
+ try {
+ boolean bInSession = activityMainBinding.startFinishCall.getText().equals("Leave session");
+ if (bInSession) {
+ leaveSession();
+ return;
+ }
+ if (arePermissionGranted()) {
+ initViews();
+ viewToConnectingState();
+
+ String sessionId = activityMainBinding.sessionName.getText().toString();
+ joinOpenViduSession(sessionId);
+ } else {
+ DialogFragment permissionsFragment = new PermissionsDialogFragment();
+ permissionsFragment.show(getSupportFragmentManager(), "Permissions Fragment");
+ }
+ }catch(Exception e){
+ Log.e(TAG, e.toString());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Initialize httpClient using the server's url and secret. This is
+ * not a secure way to perform operations from the client-side -- convert to
+ * login and user tokens in production.
+ */
+ private void initHttpClientUnsecure(){
+ if( this.httpClient != null){
+ this.httpClient.dispose();
+ }
+ OPENVIDU_URL = activityMainBinding.openviduUrl.getText().toString();
+ OPENVIDU_SECRET = activityMainBinding.openviduSecret.getText().toString();
+ httpClient = new CustomHttpClient(OPENVIDU_URL, OpenViduUtils.getBasicAuthString(OPENVIDU_SECRET));
+ }
+
+ /**
+ * If authentication is successful, enable the ability to join sessions.
+ */
+ private void verifyOpenViduAuthentication(){
+ try {
+ httpClient.httpGet(CONFIG_URL, new Callback() {
+ @SuppressLint("DefaultLocale")
+ @Override
+ public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException
+ {
+ if( !response.isSuccessful() ){
+ Log.e(TAG, String.format("verifyOpenViduAuthentication(%d) - Response not successful", response.code()));
+ runOnUiThread( () -> {
+ Toast.makeText( getApplicationContext(),
+ String.format("verifyOpenViduAuthentication(%d) - Response not successful", response.code()),
+ Toast.LENGTH_SHORT).show();
+ });
+ return;
+ }
+ runOnUiThread( () -> {
+ Toast.makeText(getApplicationContext(), "Authentication Successful", Toast.LENGTH_SHORT).show();
+ viewToLoggedInState();
+ });
+ }
+
+ @Override
+ public void onFailure(@NotNull Call call, @NotNull IOException e) {
+ runOnUiThread( () -> {
+ Toast.makeText(getApplicationContext(), "Authentication Failed", Toast.LENGTH_LONG).show();
+ });
+ }
+
+ });
+ }catch( IOException e ){
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Create and/or join sessionId
+ *
+ * @param sessionId The customSessionId of the session.
+ */
+ private void joinOpenViduSession(String sessionId) {
+ try {
+ // HTTP POST sessionId to SESSION_URL
+ MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
+ String contentString = String.format("{\"customSessionId\": \"%s\"}", sessionId);
+ RequestBody sessionBody = RequestBody.create(contentString, jsonMediaType);
+ this.httpClient.httpCall(SESSION_URL, METHOD_POST, "application/json", sessionBody, new Callback(){
+ /**
+ * WARNING: response.body().string() != response.body().toString(), e.g.
+ * toString() = okhttp3.internal.http.RealResponseBody@fe18513
+ * string() = {"id":"SessionA","createdAt":1593059100440}
+ *
+ * @param call
+ * @param response
+ * @throws IOException
+ */
+ @Override
+ public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException
+ {
+ Log.d(TAG, String.format("joinOpenViduSession.httpCall(SESSION_URL).onResponse(): code = %d, body = %s", response.code(), response.body().string()));
+
+ // For example,
+ // If no session exists, code = 200 and response.body().string() = {"id":"SessionA","createdAt":1593059100440}
+ // If session already exists, code = 409 and response.body().string() is empty
+ if( response.code() != 200 && response.code() != 409 ) {
+ Log.e(TAG, "JoinOpenViduSession.httpCall(SESSION_URL) - unhandled error code");
+ return;
+ }
+
+ final String tokenRequestContent = "{\"session\": \"" + sessionId + "\"}";
+ final RequestBody tokenRequestBody = RequestBody.create(tokenRequestContent, jsonMediaType);
+ final String tokenContentType = "application/json";
+ httpClient.httpCall(TOKEN_URL, METHOD_POST, tokenContentType, tokenRequestBody, new Callback() {
+ @Override
+ public void onResponse(@NotNull Call call, @NotNull Response response) {
+ if( !response.isSuccessful() ){
+ Log.e(TAG, "joinOpenViduSession().httpCall(TOKEN_URL).onResponse() - Failed to get token");
+ return;
+ }
+
+ try {
+ // @requireNonNull - Throws NullPointerException with message
+ String jsonBodyString = Objects.requireNonNull(response.body(), "response body empty").string();
+ Log.d(TAG, String.format("joinOpenViduSession().httpCall(TOKEN_URL): jsonBody = %s", jsonBodyString));
+
+ if( jsonBodyString.isEmpty() ){
+ Log.e(TAG, "JoinOpenViduSession().httpCall(TOKEN_URL): response empty");
+ return;
+ }
+ try {
+ JSONObject tokenJsonObject = new JSONObject(jsonBodyString);
+ String tokenString = tokenJsonObject.getString("token");
+ onSessionTokenReceived(tokenString, sessionId);
+ } catch (JSONException e) {
+ Log.e(TAG, String.valueOf(e));
+ e.printStackTrace();
+ }
+ }catch(NullPointerException | IOException e){
+ Log.e(TAG, String.valueOf(e));
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onFailure(@NotNull Call call, @NotNull IOException e) {
+ Log.e(TAG, "Error POST /api/tokens", e);
+ connectionError();
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(@NotNull Call call, @NotNull IOException e) {
+ Log.e(TAG, "Error POST /api/sessions", e);
+ connectionError();
+ }
+ });
+ } catch (IOException e) {
+ Log.e(TAG, "Error getting token", e);
+ e.printStackTrace();
+ connectionError();
+ }
+ }
+
+ private void onSessionTokenReceived(String token, String sessionId) {
+ session = new Session(sessionId, token, activityMainBinding.viewsContainer, this);
+
+ // Initialize our local participant and start local camera
+ String participantName = activityMainBinding.participantName.getText().toString();
+ LocalParticipant localParticipant = new LocalParticipant(
+ participantName,
+ session,
+ this.getApplicationContext(),
+ activityMainBinding.localGlSurfaceView);
+ localParticipant.startCamera();
+
+ // Update local participant view
+ runOnUiThread(() -> {
+ String participantNameString = activityMainBinding.participantName.getText().toString();
+ activityMainBinding.mainParticipant.setText(participantNameString);
+ activityMainBinding.mainParticipant.setPadding(20, 3, 20, 3);
+ });
+
+ // Initialize and connect the websocket to OpenVidu Server
+ startWebSocket();
+ }
+
+ /**
+ * Start custom OpenVidu web socket
+ */
+ private void startWebSocket() {
+ CustomWebSocket webSocket = new CustomWebSocket(session, OPENVIDU_URL, this);
+ webSocket.execute();
+ session.setWebSocket(webSocket);
+ }
+
+ /**
+ * On connection error, display a toast and transition the UI to disconnected state.
+ */
+ private void connectionError() {
+ Runnable myRunnable = () -> {
+ Toast toast = Toast.makeText(this, "Error connecting to " + OPENVIDU_URL, Toast.LENGTH_LONG);
+ toast.show();
+ viewToDisconnectedState();
+ };
+ new Handler(this.getMainLooper()).post(myRunnable);
+ }
+
+ /**
+ *
+ */
+ private void viewToLoggedOutState(){
+ bAuthenticated = false;
+ activityMainBinding.sessionName.setEnabled(false);
+ activityMainBinding.participantName.setEnabled(false);
+ activityMainBinding.startFinishCall.setEnabled(false);
+ }
+
+ /**
+ *
+ */
+ private void viewToLoggedInState(){
+ bAuthenticated = true;
+ activityMainBinding.sessionName.setEnabled(true);
+ activityMainBinding.participantName.setEnabled(true);
+ activityMainBinding.startFinishCall.setEnabled(true);
+ }
+
+ public void viewToDisconnectedState() {
+ runOnUiThread(() -> {
+ activityMainBinding.localGlSurfaceView.clearImage();
+ activityMainBinding.localGlSurfaceView.release();
+ activityMainBinding.startFinishCall.setText(getResources().getString(R.string.start_button));
+ activityMainBinding.startFinishCall.setEnabled(true);
+ activityMainBinding.openviduUrl.setEnabled(true);
+ activityMainBinding.openviduUrl.setFocusableInTouchMode(true);
+ activityMainBinding.openviduSecret.setEnabled(true);
+ activityMainBinding.openviduSecret.setFocusableInTouchMode(true);
+ activityMainBinding.sessionName.setEnabled(true);
+ activityMainBinding.sessionName.setFocusableInTouchMode(true);
+ activityMainBinding.participantName.setEnabled(true);
+ activityMainBinding.participantName.setFocusableInTouchMode(true);
+ activityMainBinding.mainParticipant.setText(null);
+ activityMainBinding.mainParticipant.setPadding(0, 0, 0, 0);
+ });
+ }
+
+ /**
+ *
+ */
+ public void viewToConnectingState() {
+ runOnUiThread(() -> {
+ activityMainBinding.startFinishCall.setEnabled(false);
+ activityMainBinding.openviduUrl.setEnabled(false);
+ activityMainBinding.openviduUrl.setFocusable(false);
+ activityMainBinding.openviduSecret.setEnabled(false);
+ activityMainBinding.openviduSecret.setFocusable(false);
+ activityMainBinding.sessionName.setEnabled(false);
+ activityMainBinding.sessionName.setFocusable(false);
+ activityMainBinding.participantName.setEnabled(false);
+ activityMainBinding.participantName.setFocusable(false);
+ });
+ }
+
+ /**
+ *
+ */
+ public void viewToConnectedState() {
+ runOnUiThread(() -> {
+ activityMainBinding.startFinishCall.setText(getResources().getString(R.string.hang_up));
+ activityMainBinding.startFinishCall.setEnabled(true);
+ });
+ }
+
+ /**
+ *
+ * @param remoteParticipant
+ */
+ public void createRemoteParticipantVideo(final RemoteParticipant remoteParticipant) {
+ Handler mainHandler = new Handler(this.getMainLooper());
+ Runnable myRunnable = () -> {
+ View rowView = this.getLayoutInflater().inflate(R.layout.peer_video, null);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp.setMargins(0, 0, 0, 20);
+ rowView.setLayoutParams(lp);
+ int rowId = View.generateViewId();
+ rowView.setId(rowId);
+ activityMainBinding.viewsContainer.addView(rowView);
+ SurfaceViewRenderer videoView = (SurfaceViewRenderer) ((ViewGroup) rowView).getChildAt(0);
+ remoteParticipant.setVideoView(videoView);
+ videoView.setMirror(false);
+ EglBase rootEglBase = EglBase.create();
+ videoView.init(rootEglBase.getEglBaseContext(), null);
+ videoView.setZOrderMediaOverlay(true);
+ View textView = ((ViewGroup) rowView).getChildAt(1);
+ remoteParticipant.setParticipantNameText((TextView) textView);
+ remoteParticipant.setView(rowView);
+
+ remoteParticipant.getParticipantNameText().setText(remoteParticipant.getParticipantName());
+ remoteParticipant.getParticipantNameText().setPadding(20, 3, 20, 3);
+ };
+ mainHandler.post(myRunnable);
+ }
+
+ /**
+ *
+ * @param stream
+ * @param remoteParticipant
+ */
+ public void setRemoteMediaStream(MediaStream stream, final RemoteParticipant remoteParticipant) {
+ final VideoTrack videoTrack = stream.videoTracks.get(0);
+ videoTrack.addSink(remoteParticipant.getVideoView());
+ runOnUiThread(() -> {
+ remoteParticipant.getVideoView().setVisibility(View.VISIBLE);
+ });
+ }
+
+ /**
+ *
+ */
+ public void leaveSession() {
+ try {
+ this.session.leaveSession();
+ this.httpClient.dispose();
+ viewToDisconnectedState();
+ }catch(Exception e){
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ *
+ * @return
+ */
+ private boolean arePermissionGranted() {
+ return (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_DENIED) &&
+ (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_DENIED);
+ }
+
+ /**
+ *
+ */
+ @Override
+ public void onBackPressed() {
+ leaveSession();
+ super.onBackPressed();
+ }
+
+ /**
+ * Initializes the surface view for the local participant.
+ */
+ private void initViews() {
+ EglBase.Context rootEglBaseContext = EglBase.create().getEglBaseContext();
+ activityMainBinding.localGlSurfaceView.init(rootEglBaseContext, null);
+ activityMainBinding.localGlSurfaceView.setMirror(true);
+ activityMainBinding.localGlSurfaceView.setEnableHardwareScaler(true);
+ activityMainBinding.localGlSurfaceView.setZOrderMediaOverlay(true);
+ }
+
+ /**
+ *
+ */
+ private void generateRandomName(){
+ Random random = new Random();
+ int randomIndex = random.nextInt(100);
+ Editable randomName = activityMainBinding.participantName.getText().append(String.valueOf(randomIndex));
+ activityMainBinding.participantName.setText( randomName );
+ }
+
+ private String getSharedPrefKey(int stringId) { return getSharedPrefString(stringId); }
+ private String getString(int stringId){ return getResources().getString(stringId); }
+}
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/constants/JsonConstants.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/constants/JsonConstants.java
new file mode 100644
index 000000000..6d0ac2ac6
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/constants/JsonConstants.java
@@ -0,0 +1,50 @@
+package io.openvidu.openvidu_android.constants;
+
+public final class JsonConstants {
+
+ // RPC incoming methods
+ public static final String PARTICIPANT_JOINED = "participantJoined";
+ public static final String PARTICIPANT_PUBLISHED = "participantPublished";
+ public static final String PARTICIPANT_UNPUBLISHED = "participantUnpublished";
+ public static final String PARTICIPANT_LEFT = "participantLeft";
+ public static final String PARTICIPANT_EVICTED = "participantEvicted";
+ public static final String RECORDING_STARTED = "recordingStarted";
+ public static final String RECORDING_STOPPED = "recordingStopped";
+ public static final String SEND_MESSAGE = "sendMessage";
+ public static final String STREAM_PROPERTY_CHANGED = "streamPropertyChanged";
+ public static final String FILTER_EVENT_DISPATCHED = "filterEventDispatched";
+ public static final String ICE_CANDIDATE = "iceCandidate";
+ public static final String MEDIA_ERROR = "mediaError";
+
+ // RPC outgoing methods
+ public static final String JOINROOM_METHOD = "joinRoom";
+ public static final String LEAVEROOM_METHOD = "leaveRoom";
+ public static final String PUBLISHVIDEO_METHOD = "publishVideo";
+ public static final String ONICECANDIDATE_METHOD = "onIceCandidate";
+ public static final String RECEIVEVIDEO_METHOD = "receiveVideoFrom";
+ public static final String UNSUBSCRIBEFROMVIDEO_METHOD = "unsubscribeFromVideo";
+ public static final String SENDMESSAGE_ROOM_METHOD = "sendMessage";
+ public static final String UNPUBLISHVIDEO_METHOD = "unpublishVideo";
+ public static final String STREAMPROPERTYCHANGED_METHOD = "streamPropertyChanged";
+ public static final String FORCEDISCONNECT_METHOD = "forceDisconnect";
+ public static final String FORCEUNPUBLISH_METHOD = "forceUnpublish";
+ public static final String APPLYFILTER_METHOD = "applyFilter";
+ public static final String EXECFILTERMETHOD_METHOD = "execFilterMethod";
+ public static final String REMOVEFILTER_METHOD = "removeFilter";
+ public static final String ADDFILTEREVENTLISTENER_METHOD = "addFilterEventListener";
+ public static final String REMOVEFILTEREVENTLISTENER_METHOD = "removeFilterEventListener";
+ public static final String PING_METHOD = "ping";
+
+ public static final String JSON_RPCVERSION = "2.0";
+
+ public static final String VALUE = "value";
+ public static final String PARAMS = "params";
+ public static final String METHOD = "method";
+ public static final String ID = "id";
+ public static final String RESULT = "result";
+
+ public static final String SESSION_ID = "sessionId";
+ public static final String SDP_ANSWER = "sdpAnswer";
+ public static final String METADATA = "metadata";
+
+}
\ No newline at end of file
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/fragments/PermissionsDialogFragment.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/fragments/PermissionsDialogFragment.java
new file mode 100644
index 000000000..9f9189801
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/fragments/PermissionsDialogFragment.java
@@ -0,0 +1,32 @@
+package io.openvidu.openvidu_android.fragments;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+import io.openvidu.openvidu_android.R;
+import io.openvidu.openvidu_android.activities.SessionActivity;
+
+/**
+ * This fragment display a permission request message to the user before
+ * invoking SessionActivity.askForPermissions
+ */
+public class PermissionsDialogFragment extends DialogFragment {
+
+ private static final String TAG = "PermissionsDialog";
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.permissions_dialog_title);
+ builder.setMessage(R.string.no_permissions_granted)
+ .setPositiveButton(R.string.accept_permissions_dialog, (dialog, id) -> ((SessionActivity) getActivity()).askForPermissions())
+ .setNegativeButton(R.string.cancel_dialog, (dialog, id) -> Log.i(TAG, "User cancelled Permissions Dialog"));
+ return builder.create();
+ }
+}
\ No newline at end of file
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomPeerConnectionObserver.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomPeerConnectionObserver.java
new file mode 100644
index 000000000..26482628d
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomPeerConnectionObserver.java
@@ -0,0 +1,75 @@
+package io.openvidu.openvidu_android.observers;
+
+import android.util.Log;
+
+import org.webrtc.DataChannel;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.RtpReceiver;
+
+import java.util.Arrays;
+
+public class CustomPeerConnectionObserver implements PeerConnection.Observer {
+
+ private String TAG = "PeerConnection";
+
+ public CustomPeerConnectionObserver(String id) {
+ this.TAG = this.TAG + "-" + id;
+ }
+
+ @Override
+ public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+ Log.d(TAG, "onSignalingChange() called with: signalingState = [" + signalingState + "]");
+ }
+
+ @Override
+ public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
+ Log.d(TAG, "onIceConnectionChange() called with: iceConnectionState = [" + iceConnectionState + "]");
+ }
+
+ @Override
+ public void onIceConnectionReceivingChange(boolean b) {
+ Log.d(TAG, "onIceConnectionReceivingChange() called with: b = [" + b + "]");
+ }
+
+ @Override
+ public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
+ Log.d(TAG, "onIceGatheringChange() called with: iceGatheringState = [" + iceGatheringState + "]");
+ }
+
+ @Override
+ public void onIceCandidate(IceCandidate iceCandidate) {
+ Log.d(TAG, "onIceCandidate() called with: iceCandidate = [" + iceCandidate + "]");
+ }
+
+ @Override
+ public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
+ Log.d(TAG, "onIceCandidatesRemoved() called with: iceCandidates = [" + Arrays.toString(iceCandidates) + "]");
+ }
+
+ @Override
+ public void onAddStream(MediaStream mediaStream) {
+ Log.d(TAG, "onAddStream() called with: mediaStream = [" + mediaStream + "]");
+ }
+
+ @Override
+ public void onRemoveStream(MediaStream mediaStream) {
+ Log.d(TAG, "onRemoveStream() called with: mediaStream = [" + mediaStream + "]");
+ }
+
+ @Override
+ public void onDataChannel(DataChannel dataChannel) {
+ Log.d(TAG, "onDataChannel() called with: dataChannel = [" + dataChannel + "]");
+ }
+
+ @Override
+ public void onRenegotiationNeeded() {
+ Log.d(TAG, "onRenegotiationNeeded() called");
+ }
+
+ @Override
+ public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
+ Log.d(TAG, "onAddTrack() called with: mediaStreams = [" + Arrays.toString(mediaStreams) + "]");
+ }
+}
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomSdpObserver.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomSdpObserver.java
new file mode 100644
index 000000000..c6824a3c9
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomSdpObserver.java
@@ -0,0 +1,39 @@
+package io.openvidu.openvidu_android.observers;
+
+import android.util.Log;
+
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+
+public class CustomSdpObserver implements SdpObserver {
+
+ private String tag;
+
+ public CustomSdpObserver(String tag) {
+ this.tag = "SdpObserver-" + tag;
+ }
+
+ private void log(String s) {
+ Log.d(tag, s);
+ }
+
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ log("onCreateSuccess " + sessionDescription);
+ }
+
+ @Override
+ public void onSetSuccess() {
+ log("onSetSuccess ");
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ log("onCreateFailure " + s);
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ log("onSetFailure " + s);
+ }
+}
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/LocalParticipant.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/LocalParticipant.java
new file mode 100644
index 000000000..1dc363373
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/LocalParticipant.java
@@ -0,0 +1,124 @@
+package io.openvidu.openvidu_android.openvidu;
+
+import android.content.Context;
+import android.os.Build;
+
+import org.webrtc.AudioSource;
+import org.webrtc.Camera1Enumerator;
+import org.webrtc.Camera2Enumerator;
+import org.webrtc.CameraEnumerator;
+import org.webrtc.EglBase;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.SessionDescription;
+import org.webrtc.SurfaceTextureHelper;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoCapturer;
+import org.webrtc.VideoSource;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class LocalParticipant extends Participant {
+
+ private Context context;
+ private SurfaceViewRenderer localVideoView;
+ private SurfaceTextureHelper surfaceTextureHelper;
+ private VideoCapturer videoCapturer;
+
+ private Collection localIceCandidates;
+ private SessionDescription localSessionDescription;
+
+ public LocalParticipant(String participantName, Session session, Context context, SurfaceViewRenderer localVideoView) {
+ super(participantName, session);
+ this.localVideoView = localVideoView;
+ this.localVideoView = localVideoView;
+ this.context = context;
+ this.participantName = participantName;
+ this.localIceCandidates = new ArrayList<>();
+ session.setLocalParticipant(this);
+ }
+
+ public void startCamera() {
+
+ final EglBase.Context eglBaseContext = EglBase.create().getEglBaseContext();
+ PeerConnectionFactory peerConnectionFactory = this.session.getPeerConnectionFactory();
+
+ // create AudioSource
+ AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints());
+ this.audioTrack = peerConnectionFactory.createAudioTrack("101", audioSource);
+
+ surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBaseContext);
+ // create VideoCapturer
+ VideoCapturer videoCapturer = createCameraCapturer();
+ VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
+ videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
+ videoCapturer.startCapture(480, 640, 30);
+
+ // create VideoTrack
+ this.videoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);
+ // display in localView
+ this.videoTrack.addSink(localVideoView);
+ }
+
+ private VideoCapturer createCameraCapturer() {
+ CameraEnumerator enumerator;
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
+ enumerator = new Camera2Enumerator(this.context);
+ } else {
+ enumerator = new Camera1Enumerator(false);
+ }
+ final String[] deviceNames = enumerator.getDeviceNames();
+
+ // Try to find front facing camera
+ for (String deviceName : deviceNames) {
+ if (enumerator.isFrontFacing(deviceName)) {
+ videoCapturer = enumerator.createCapturer(deviceName, null);
+ if (videoCapturer != null) {
+ return videoCapturer;
+ }
+ }
+ }
+ // Front facing camera not found, try something else
+ for (String deviceName : deviceNames) {
+ if (!enumerator.isFrontFacing(deviceName)) {
+ videoCapturer = enumerator.createCapturer(deviceName, null);
+ if (videoCapturer != null) {
+ return videoCapturer;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void storeIceCandidate(IceCandidate iceCandidate) {
+ localIceCandidates.add(iceCandidate);
+ }
+
+ public Collection getLocalIceCandidates() {
+ return this.localIceCandidates;
+ }
+
+ public void storeLocalSessionDescription(SessionDescription sessionDescription) {
+ localSessionDescription = sessionDescription;
+ }
+
+ public SessionDescription getLocalSessionDescription() {
+ return this.localSessionDescription;
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (videoTrack != null) {
+ videoTrack.removeSink(localVideoView);
+ videoCapturer.dispose();
+ videoCapturer = null;
+ }
+ if (surfaceTextureHelper != null) {
+ surfaceTextureHelper.dispose();
+ surfaceTextureHelper = null;
+ }
+ }
+}
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Participant.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Participant.java
new file mode 100644
index 000000000..704ab032a
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Participant.java
@@ -0,0 +1,93 @@
+package io.openvidu.openvidu_android.openvidu;
+
+import android.util.Log;
+
+import org.webrtc.AudioTrack;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.VideoTrack;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class Participant {
+
+ protected String connectionId;
+ protected String participantName;
+ protected Session session;
+ protected List iceCandidateList = new ArrayList<>();
+ protected PeerConnection peerConnection;
+ protected AudioTrack audioTrack;
+ protected VideoTrack videoTrack;
+ protected MediaStream mediaStream;
+
+ public Participant(String participantName, Session session) {
+ this.participantName = participantName;
+ this.session = session;
+ }
+
+ public Participant(String connectionId, String participantName, Session session) {
+ this.connectionId = connectionId;
+ this.participantName = participantName;
+ this.session = session;
+ }
+
+ public String getConnectionId() {
+ return this.connectionId;
+ }
+
+ public void setConnectionId(String connectionId) {
+ this.connectionId = connectionId;
+ }
+
+ public String getParticipantName() {
+ return this.participantName;
+ }
+
+ public List getIceCandidateList() {
+ return this.iceCandidateList;
+ }
+
+ public PeerConnection getPeerConnection() {
+ return peerConnection;
+ }
+
+ public void setPeerConnection(PeerConnection peerConnection) {
+ this.peerConnection = peerConnection;
+ }
+
+ public AudioTrack getAudioTrack() {
+ return this.audioTrack;
+ }
+
+ public void setAudioTrack(AudioTrack audioTrack) {
+ this.audioTrack = audioTrack;
+ }
+
+ public VideoTrack getVideoTrack() {
+ return this.videoTrack;
+ }
+
+ public void setVideoTrack(VideoTrack videoTrack) {
+ this.videoTrack = videoTrack;
+ }
+
+ public MediaStream getMediaStream() {
+ return this.mediaStream;
+ }
+
+ public void setMediaStream(MediaStream mediaStream) {
+ this.mediaStream = mediaStream;
+ }
+
+ public void dispose() {
+ if (this.peerConnection != null) {
+ try {
+ this.peerConnection.close();
+ } catch (IllegalStateException e) {
+ Log.e("Dispose PeerConnection", e.getMessage());
+ }
+ }
+ }
+}
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/RemoteParticipant.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/RemoteParticipant.java
new file mode 100644
index 000000000..2ce739b52
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/RemoteParticipant.java
@@ -0,0 +1,47 @@
+package io.openvidu.openvidu_android.openvidu;
+
+import android.view.View;
+import android.widget.TextView;
+
+import org.webrtc.SurfaceViewRenderer;
+
+public class RemoteParticipant extends Participant {
+
+ private View view;
+ private SurfaceViewRenderer videoView;
+ private TextView participantNameText;
+
+ public RemoteParticipant(String connectionId, String participantName, Session session) {
+ super(connectionId, participantName, session);
+ this.session.addRemoteParticipant(this);
+ }
+
+ public View getView() {
+ return this.view;
+ }
+
+ public void setView(View view) {
+ this.view = view;
+ }
+
+ public SurfaceViewRenderer getVideoView() {
+ return this.videoView;
+ }
+
+ public void setVideoView(SurfaceViewRenderer videoView) {
+ this.videoView = videoView;
+ }
+
+ public TextView getParticipantNameText() {
+ return this.participantNameText;
+ }
+
+ public void setParticipantNameText(TextView participantNameText) {
+ this.participantNameText = participantNameText;
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ }
+}
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Session.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Session.java
new file mode 100644
index 000000000..b71797db0
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Session.java
@@ -0,0 +1,220 @@
+package io.openvidu.openvidu_android.openvidu;
+
+import android.util.Log;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import io.openvidu.openvidu_android.activities.SessionActivity;
+import io.openvidu.openvidu_android.observers.CustomPeerConnectionObserver;
+import io.openvidu.openvidu_android.observers.CustomSdpObserver;
+import io.openvidu.openvidu_android.websocket.CustomWebSocket;
+
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.RtpReceiver;
+import org.webrtc.RtpTransceiver;
+import org.webrtc.SessionDescription;
+import org.webrtc.SoftwareVideoDecoderFactory;
+import org.webrtc.SoftwareVideoEncoderFactory;
+import org.webrtc.VideoDecoderFactory;
+import org.webrtc.VideoEncoderFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class Session {
+
+ private LocalParticipant localParticipant;
+ private Map remoteParticipants = new HashMap<>();
+ private String id;
+ private String token;
+ private LinearLayout views_container;
+ private PeerConnectionFactory peerConnectionFactory;
+ private CustomWebSocket websocket;
+ private SessionActivity activity;
+
+ public Session(String id, String token, LinearLayout views_container, SessionActivity activity) {
+ this.id = id;
+ this.token = token;
+ this.views_container = views_container;
+ this.activity = activity;
+
+ PeerConnectionFactory.InitializationOptions.Builder optionsBuilder = PeerConnectionFactory.InitializationOptions.builder(activity.getApplicationContext());
+ optionsBuilder.setEnableInternalTracer(true);
+ PeerConnectionFactory.InitializationOptions opt = optionsBuilder.createInitializationOptions();
+ PeerConnectionFactory.initialize(opt);
+ PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
+
+ final VideoEncoderFactory encoderFactory;
+ final VideoDecoderFactory decoderFactory;
+ encoderFactory = new SoftwareVideoEncoderFactory();
+ decoderFactory = new SoftwareVideoDecoderFactory();
+
+ peerConnectionFactory = PeerConnectionFactory.builder()
+ .setVideoEncoderFactory(encoderFactory)
+ .setVideoDecoderFactory(decoderFactory)
+ .setOptions(options)
+ .createPeerConnectionFactory();
+ }
+
+ public void setWebSocket(CustomWebSocket websocket) {
+ this.websocket = websocket;
+ }
+
+ public PeerConnection createLocalPeerConnection() {
+ final List iceServers = new ArrayList<>();
+ PeerConnection.IceServer iceServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer();
+ iceServers.add(iceServer);
+
+ PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
+ rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED;
+ rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
+ rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE;
+ rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
+ rtcConfig.keyType = PeerConnection.KeyType.ECDSA;
+ rtcConfig.enableDtlsSrtp = true;
+ rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
+
+ PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new CustomPeerConnectionObserver("local") {
+ @Override
+ public void onIceCandidate(IceCandidate iceCandidate) {
+ super.onIceCandidate(iceCandidate);
+ websocket.onIceCandidate(iceCandidate, localParticipant.getConnectionId());
+ }
+ });
+
+ return peerConnection;
+ }
+
+ public void createRemotePeerConnection(final String connectionId) {
+ final List iceServers = new ArrayList<>();
+ PeerConnection.IceServer iceServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer();
+ iceServers.add(iceServer);
+
+ PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
+ rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED;
+ rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
+ rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE;
+ rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
+ rtcConfig.keyType = PeerConnection.KeyType.ECDSA;
+ rtcConfig.enableDtlsSrtp = true;
+ rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
+
+ PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new CustomPeerConnectionObserver("remotePeerCreation") {
+ @Override
+ public void onIceCandidate(IceCandidate iceCandidate) {
+ super.onIceCandidate(iceCandidate);
+ websocket.onIceCandidate(iceCandidate, connectionId);
+ }
+
+ @Override
+ public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
+ super.onAddTrack(rtpReceiver, mediaStreams);
+ activity.setRemoteMediaStream(mediaStreams[0], remoteParticipants.get(connectionId));
+ }
+
+ @Override
+ public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+ if (PeerConnection.SignalingState.STABLE.equals(signalingState)) {
+ final RemoteParticipant remoteParticipant = remoteParticipants.get(connectionId);
+ Iterator it = remoteParticipant.getIceCandidateList().iterator();
+ while (it.hasNext()) {
+ IceCandidate candidate = it.next();
+ remoteParticipant.getPeerConnection().addIceCandidate(candidate);
+ it.remove();
+ }
+ }
+ }
+ });
+
+ peerConnection.addTrack(localParticipant.getAudioTrack());//Add audio track to create transReceiver
+ peerConnection.addTrack(localParticipant.getVideoTrack());//Add video track to create transReceiver
+
+ for (RtpTransceiver transceiver : peerConnection.getTransceivers()) {
+ //We set both audio and video in receive only mode
+ transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY);
+ }
+
+ this.remoteParticipants.get(connectionId).setPeerConnection(peerConnection);
+ }
+
+ public void createLocalOffer(MediaConstraints constraints) {
+ localParticipant.getPeerConnection().createOffer(new CustomSdpObserver("local offer sdp") {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ super.onCreateSuccess(sessionDescription);
+ Log.i("createOffer SUCCESS", sessionDescription.toString());
+ localParticipant.getPeerConnection().setLocalDescription(new CustomSdpObserver("local set local"), sessionDescription);
+ websocket.publishVideo(sessionDescription);
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e("createOffer ERROR", s);
+ }
+
+ }, constraints);
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public String getToken() {
+ return this.token;
+ }
+
+ public LocalParticipant getLocalParticipant() {
+ return this.localParticipant;
+ }
+
+ public void setLocalParticipant(LocalParticipant localParticipant) {
+ this.localParticipant = localParticipant;
+ }
+
+ public RemoteParticipant getRemoteParticipant(String id) {
+ return this.remoteParticipants.get(id);
+ }
+
+ public PeerConnectionFactory getPeerConnectionFactory() {
+ return this.peerConnectionFactory;
+ }
+
+ public void addRemoteParticipant(RemoteParticipant remoteParticipant) {
+ this.remoteParticipants.put(remoteParticipant.getConnectionId(), remoteParticipant);
+ }
+
+ public RemoteParticipant removeRemoteParticipant(String id) {
+ return this.remoteParticipants.remove(id);
+ }
+
+ public void leaveSession() {
+ websocket.setWebsocketCancelled(true);
+ if (websocket != null) {
+ websocket.leaveRoom();
+ websocket.disconnect();
+ }
+ this.localParticipant.dispose();
+ for (RemoteParticipant remoteParticipant : remoteParticipants.values()) {
+ if (remoteParticipant.getPeerConnection() != null) {
+ remoteParticipant.getPeerConnection().close();
+ }
+ views_container.removeView(remoteParticipant.getView());
+ }
+ if (peerConnectionFactory != null) {
+ peerConnectionFactory.dispose();
+ peerConnectionFactory = null;
+ }
+ }
+
+ public void removeView(View view) {
+ this.views_container.removeView(view);
+ }
+
+}
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/CustomHttpClient.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/CustomHttpClient.java
new file mode 100644
index 000000000..6978c844f
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/CustomHttpClient.java
@@ -0,0 +1,105 @@
+package io.openvidu.openvidu_android.utils;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+
+public class CustomHttpClient {
+
+ private OkHttpClient client;
+ private String baseUrl;
+ private String basicAuth;
+
+ public CustomHttpClient(String baseUrl, String basicAuth) {
+ this.baseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
+ this.basicAuth = basicAuth;
+
+ try {
+ // Create a trust manager that does not validate certificate chains
+ final TrustManager[] trustAllCerts = new TrustManager[]{
+ new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ }
+ };
+
+ // Install the all-trusting trust manager
+ final SSLContext sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null, trustAllCerts, new SecureRandom());
+ // Create an ssl socket factory with our all-trusting manager
+ final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+
+ this.client = new OkHttpClient.Builder().sslSocketFactory(sslSocketFactory, new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new java.security.cert.X509Certificate[]{};
+ }
+ }).hostnameVerifier(new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ }).build();
+
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void httpCall(String url, String method, String contentType, RequestBody body, Callback callback) throws IOException {
+ url = url.startsWith("/") ? url.substring(1) : url;
+ Request request = new Request.Builder()
+ .url(this.baseUrl + url)
+ .header("Authorization", this.basicAuth).header("Content-Type", contentType).method(method, body)
+ .build();
+ Call call = client.newCall(request);
+ call.enqueue(callback);
+ }
+
+ public void httpGet(String url, Callback callback) throws IOException{
+ url = url.startsWith("/") ? url.substring(1) : url;
+ Request request = new Request.Builder()
+ .url(this.baseUrl + url)
+ .header("Authorization", this.basicAuth)
+ .build();
+ Call call = client.newCall(request);
+ call.enqueue(callback);
+ }
+
+ public void dispose() {
+ this.client.dispatcher().executorService().shutdown();
+ }
+
+}
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/OpenViduUtils.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/OpenViduUtils.java
new file mode 100644
index 000000000..bb5e7684e
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/OpenViduUtils.java
@@ -0,0 +1,22 @@
+package io.openvidu.openvidu_android.utils;
+
+import android.text.Editable;
+import android.util.Base64;
+
+import java.util.Random;
+
+/**
+ * Contains static methods
+ */
+public class OpenViduUtils {
+
+ /**
+ *
+ * @param secret
+ * @return
+ */
+ public static String getBasicAuthString(String secret){
+ String encodedSecretString = android.util.Base64.encodeToString(("OPENVIDUAPP:" + secret).getBytes(), Base64.DEFAULT);
+ return String.format("Basic %s", encodedSecretString.trim());
+ }
+}
diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/websocket/CustomWebSocket.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/websocket/CustomWebSocket.java
new file mode 100644
index 000000000..80cb022a1
--- /dev/null
+++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/websocket/CustomWebSocket.java
@@ -0,0 +1,588 @@
+package io.openvidu.openvidu_android.websocket;
+
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.neovisionaries.ws.client.ThreadType;
+import com.neovisionaries.ws.client.WebSocket;
+import com.neovisionaries.ws.client.WebSocketException;
+import com.neovisionaries.ws.client.WebSocketFactory;
+import com.neovisionaries.ws.client.WebSocketFrame;
+import com.neovisionaries.ws.client.WebSocketListener;
+import com.neovisionaries.ws.client.WebSocketState;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.RtpTransceiver;
+import org.webrtc.SessionDescription;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import io.openvidu.openvidu_android.activities.SessionActivity;
+import io.openvidu.openvidu_android.constants.JsonConstants;
+import io.openvidu.openvidu_android.observers.CustomSdpObserver;
+import io.openvidu.openvidu_android.openvidu.LocalParticipant;
+import io.openvidu.openvidu_android.openvidu.Participant;
+import io.openvidu.openvidu_android.openvidu.RemoteParticipant;
+import io.openvidu.openvidu_android.openvidu.Session;
+
+public class CustomWebSocket extends AsyncTask implements WebSocketListener {
+
+ private final String TAG = "CustomWebSocketListener";
+ private final int PING_MESSAGE_INTERVAL = 5;
+ private final TrustManager[] trustManagers = new TrustManager[]{new X509TrustManager() {
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ @Override
+ public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
+ Log.i(TAG, ": authType: " + authType);
+ }
+
+ @Override
+ public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
+ Log.i(TAG, ": authType: " + authType);
+ }
+ }};
+ private AtomicInteger RPC_ID = new AtomicInteger(0);
+ private AtomicInteger ID_PING = new AtomicInteger(-1);
+ private AtomicInteger ID_JOINROOM = new AtomicInteger(-1);
+ private AtomicInteger ID_LEAVEROOM = new AtomicInteger(-1);
+ private AtomicInteger ID_PUBLISHVIDEO = new AtomicInteger(-1);
+ private Map IDS_RECEIVEVIDEO = new ConcurrentHashMap<>();
+ private Set IDS_ONICECANDIDATE = Collections.newSetFromMap(new ConcurrentHashMap<>());
+ private Session session;
+ private String openviduUrl;
+ private SessionActivity activity;
+ private WebSocket websocket;
+ private boolean websocketCancelled = false;
+
+ public CustomWebSocket(Session session, String openviduUrl, SessionActivity activity) {
+ this.session = session;
+ this.openviduUrl = openviduUrl;
+ this.activity = activity;
+ }
+
+ @Override
+ public void onTextMessage(WebSocket websocket, String text) throws Exception {
+ Log.i(TAG, "Text Message " + text);
+ JSONObject json = new JSONObject(text);
+ if (json.has(JsonConstants.RESULT)) {
+ handleServerResponse(json);
+ } else {
+ handleServerEvent(json);
+ }
+ }
+
+ private void handleServerResponse(JSONObject json) throws
+ JSONException {
+ final int rpcId = json.getInt(JsonConstants.ID);
+ JSONObject result = new JSONObject(json.getString(JsonConstants.RESULT));
+
+ if (result.has("value") && result.getString("value").equals("pong")) {
+ // Response to ping
+ Log.i(TAG, "pong");
+
+ } else if (rpcId == this.ID_JOINROOM.get()) {
+ // Response to joinRoom
+ activity.viewToConnectedState();
+
+ final LocalParticipant localParticipant = this.session.getLocalParticipant();
+ final String localConnectionId = result.getString(JsonConstants.ID);
+ localParticipant.setConnectionId(localConnectionId);
+
+ PeerConnection localPeerConnection = session.createLocalPeerConnection();
+
+ localPeerConnection.addTrack(localParticipant.getAudioTrack());
+ localPeerConnection.addTrack(localParticipant.getVideoTrack());
+
+ for (RtpTransceiver transceiver : localPeerConnection.getTransceivers()) {
+ transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY);
+ }
+
+ localParticipant.setPeerConnection(localPeerConnection);
+
+ MediaConstraints sdpConstraints = new MediaConstraints();
+ sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true"));
+ sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true"));
+ session.createLocalOffer(sdpConstraints);
+
+ if (result.getJSONArray(JsonConstants.VALUE).length() > 0) {
+ // There were users already connected to the session
+ addRemoteParticipantsAlreadyInRoom(result);
+ }
+
+ } else if (rpcId == this.ID_LEAVEROOM.get()) {
+ // Response to leaveRoom
+ if (websocket.isOpen()) {
+ websocket.disconnect();
+ }
+
+ } else if (rpcId == this.ID_PUBLISHVIDEO.get()) {
+ // Response to publishVideo
+ SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, result.getString("sdpAnswer"));
+ this.session.getLocalParticipant().getPeerConnection().setRemoteDescription(new CustomSdpObserver("localSetRemoteDesc"), sessionDescription);
+
+ } else if (this.IDS_RECEIVEVIDEO.containsKey(rpcId)) {
+ // Response to receiveVideoFrom
+ SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, result.getString("sdpAnswer"));
+ session.getRemoteParticipant(IDS_RECEIVEVIDEO.remove(rpcId)).getPeerConnection().setRemoteDescription(new CustomSdpObserver("remoteSetRemoteDesc"), sessionDescription);
+
+ } else if (this.IDS_ONICECANDIDATE.contains(rpcId)) {
+ // Response to onIceCandidate
+ IDS_ONICECANDIDATE.remove(rpcId);
+
+ } else {
+ Log.e(TAG, "Unrecognized server response: " + result);
+ }
+ }
+
+ public void joinRoom() {
+ Map joinRoomParams = new HashMap<>();
+ joinRoomParams.put(JsonConstants.METADATA, "{\"clientData\": \"" + this.session.getLocalParticipant().getParticipantName() + "\"}");
+ joinRoomParams.put("secret", "");
+ joinRoomParams.put("session", this.session.getId());
+ joinRoomParams.put("platform", "Android " + android.os.Build.VERSION.SDK_INT);
+ joinRoomParams.put("token", this.session.getToken());
+ this.ID_JOINROOM.set(this.sendJson(JsonConstants.JOINROOM_METHOD, joinRoomParams));
+ }
+
+ public void leaveRoom() {
+ this.ID_LEAVEROOM.set(this.sendJson(JsonConstants.LEAVEROOM_METHOD));
+ }
+
+ public void publishVideo(SessionDescription sessionDescription) {
+ Map publishVideoParams = new HashMap<>();
+ publishVideoParams.put("audioActive", "true");
+ publishVideoParams.put("videoActive", "true");
+ publishVideoParams.put("doLoopback", "false");
+ publishVideoParams.put("frameRate", "30");
+ publishVideoParams.put("hasAudio", "true");
+ publishVideoParams.put("hasVideo", "true");
+ publishVideoParams.put("typeOfVideo", "CAMERA");
+ publishVideoParams.put("videoDimensions", "{\"width\":320, \"height\":240}");
+ publishVideoParams.put("sdpOffer", sessionDescription.description);
+ this.ID_PUBLISHVIDEO.set(this.sendJson(JsonConstants.PUBLISHVIDEO_METHOD, publishVideoParams));
+ }
+
+ public void receiveVideoFrom(SessionDescription sessionDescription, RemoteParticipant remoteParticipant, String streamId) {
+ Map receiveVideoFromParams = new HashMap<>();
+ receiveVideoFromParams.put("sdpOffer", sessionDescription.description);
+ receiveVideoFromParams.put("sender", streamId);
+ this.IDS_RECEIVEVIDEO.put(this.sendJson(JsonConstants.RECEIVEVIDEO_METHOD, receiveVideoFromParams), remoteParticipant.getConnectionId());
+ }
+
+ public void onIceCandidate(IceCandidate iceCandidate, String endpointName) {
+ Map onIceCandidateParams = new HashMap<>();
+ if (endpointName != null) {
+ onIceCandidateParams.put("endpointName", endpointName);
+ }
+ onIceCandidateParams.put("candidate", iceCandidate.sdp);
+ onIceCandidateParams.put("sdpMid", iceCandidate.sdpMid);
+ onIceCandidateParams.put("sdpMLineIndex", Integer.toString(iceCandidate.sdpMLineIndex));
+ this.IDS_ONICECANDIDATE.add(this.sendJson(JsonConstants.ONICECANDIDATE_METHOD, onIceCandidateParams));
+ }
+
+ private void handleServerEvent(JSONObject json) throws JSONException {
+ if (!json.has(JsonConstants.PARAMS)) {
+ Log.e(TAG, "No params " + json.toString());
+ } else {
+ final JSONObject params = new JSONObject(json.getString(JsonConstants.PARAMS));
+ String method = json.getString(JsonConstants.METHOD);
+ switch (method) {
+ case JsonConstants.ICE_CANDIDATE:
+ iceCandidateEvent(params);
+ break;
+ case JsonConstants.PARTICIPANT_JOINED:
+ participantJoinedEvent(params);
+ break;
+ case JsonConstants.PARTICIPANT_PUBLISHED:
+ participantPublishedEvent(params);
+ break;
+ case JsonConstants.PARTICIPANT_LEFT:
+ participantLeftEvent(params);
+ break;
+ default:
+ throw new JSONException("Unknown method: " + method);
+ }
+ }
+ }
+
+ public int sendJson(String method) {
+ return this.sendJson(method, new HashMap<>());
+ }
+
+ public synchronized int sendJson(String method, Map params) {
+ final int id = RPC_ID.get();
+ JSONObject jsonObject = new JSONObject();
+ try {
+ JSONObject paramsJson = new JSONObject();
+ for (Map.Entry param : params.entrySet()) {
+ paramsJson.put(param.getKey(), param.getValue());
+ }
+ jsonObject.put("jsonrpc", JsonConstants.JSON_RPCVERSION);
+ jsonObject.put("method", method);
+ jsonObject.put("id", id);
+ jsonObject.put("params", paramsJson);
+ } catch (JSONException e) {
+ Log.i(TAG, "JSONException raised on sendJson", e);
+ return -1;
+ }
+ this.websocket.sendText(jsonObject.toString());
+ RPC_ID.incrementAndGet();
+ return id;
+ }
+
+ private void addRemoteParticipantsAlreadyInRoom(JSONObject result) throws
+ JSONException {
+ for (int i = 0; i < result.getJSONArray(JsonConstants.VALUE).length(); i++) {
+ JSONObject participantJson = result.getJSONArray(JsonConstants.VALUE).getJSONObject(i);
+ RemoteParticipant remoteParticipant = this.newRemoteParticipantAux(participantJson);
+ try {
+ JSONArray streams = participantJson.getJSONArray("streams");
+ for (int j = 0; j < streams.length(); j++) {
+ JSONObject stream = streams.getJSONObject(0);
+ String streamId = stream.getString("id");
+ this.subscribeAux(remoteParticipant, streamId);
+ }
+ } catch (Exception e) {
+ //Sometimes when we enter in room the other participants have no stream
+ //We catch that in this way the iteration of participants doesn't stop
+ Log.e(TAG, "Error in addRemoteParticipantsAlreadyInRoom: " + e.getLocalizedMessage());
+ }
+ }
+ }
+
+ private void iceCandidateEvent(JSONObject params) throws JSONException {
+ IceCandidate iceCandidate = new IceCandidate(params.getString("sdpMid"), params.getInt("sdpMLineIndex"), params.getString("candidate"));
+ final String connectionId = params.getString("senderConnectionId");
+ boolean isRemote = !session.getLocalParticipant().getConnectionId().equals(connectionId);
+ final Participant participant = isRemote ? session.getRemoteParticipant(connectionId) : session.getLocalParticipant();
+ final PeerConnection pc = participant.getPeerConnection();
+
+ switch (pc.signalingState()) {
+ case CLOSED:
+ Log.e("saveIceCandidate error", "PeerConnection object is closed");
+ break;
+ case STABLE:
+ if (pc.getRemoteDescription() != null) {
+ participant.getPeerConnection().addIceCandidate(iceCandidate);
+ } else {
+ participant.getIceCandidateList().add(iceCandidate);
+ }
+ break;
+ default:
+ participant.getIceCandidateList().add(iceCandidate);
+ }
+ }
+
+ private void participantJoinedEvent(JSONObject params) throws JSONException {
+ this.newRemoteParticipantAux(params);
+ }
+
+ private void participantPublishedEvent(JSONObject params) throws
+ JSONException {
+ String remoteParticipantId = params.getString(JsonConstants.ID);
+ final RemoteParticipant remoteParticipant = this.session.getRemoteParticipant(remoteParticipantId);
+ final String streamId = params.getJSONArray("streams").getJSONObject(0).getString("id");
+ this.subscribeAux(remoteParticipant, streamId);
+ }
+
+ private void participantLeftEvent(JSONObject params) throws JSONException {
+ final RemoteParticipant remoteParticipant = this.session.removeRemoteParticipant(params.getString("connectionId"));
+ remoteParticipant.dispose();
+ Handler mainHandler = new Handler(activity.getMainLooper());
+ Runnable myRunnable = () -> session.removeView(remoteParticipant.getView());
+ mainHandler.post(myRunnable);
+ }
+
+ private RemoteParticipant newRemoteParticipantAux(JSONObject participantJson) throws JSONException {
+ final String connectionId = participantJson.getString(JsonConstants.ID);
+ String participantName = "";
+ if (participantJson.getString(JsonConstants.METADATA) != null) {
+ String jsonStringified = participantJson.getString(JsonConstants.METADATA);
+ try {
+ JSONObject json = new JSONObject(jsonStringified);
+ String clientData = json.getString("clientData");
+ if (clientData != null) {
+ participantName = clientData;
+ }
+ } catch(JSONException e) {
+ participantName = jsonStringified;
+ }
+ }
+ final RemoteParticipant remoteParticipant = new RemoteParticipant(connectionId, participantName, this.session);
+ this.activity.createRemoteParticipantVideo(remoteParticipant);
+ this.session.createRemotePeerConnection(remoteParticipant.getConnectionId());
+ return remoteParticipant;
+ }
+
+ private void subscribeAux(RemoteParticipant remoteParticipant, String streamId) {
+ MediaConstraints sdpConstraints = new MediaConstraints();
+ sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true"));
+ sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true"));
+
+ remoteParticipant.getPeerConnection().createOffer(new CustomSdpObserver("remote offer sdp") {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ super.onCreateSuccess(sessionDescription);
+ remoteParticipant.getPeerConnection().setLocalDescription(new CustomSdpObserver("remoteSetLocalDesc"), sessionDescription);
+ receiveVideoFrom(sessionDescription, remoteParticipant, streamId);
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e("createOffer error", s);
+ }
+ }, sdpConstraints);
+ }
+
+ public void setWebsocketCancelled(boolean websocketCancelled) {
+ this.websocketCancelled = websocketCancelled;
+ }
+
+ public void disconnect() {
+ this.websocket.disconnect();
+ }
+
+ @Override
+ public void onStateChanged(WebSocket websocket, WebSocketState newState) throws Exception {
+ Log.i(TAG, "State changed: " + newState.name());
+ }
+
+ @Override
+ public void onConnected(WebSocket ws, Map> headers) throws
+ Exception {
+ Log.i(TAG, "Connected");
+ pingMessageHandler();
+ this.joinRoom();
+ }
+
+ @Override
+ public void onConnectError(WebSocket websocket, WebSocketException cause) throws Exception {
+ Log.e(TAG, "Connect error: " + cause);
+ }
+
+ @Override
+ public void onDisconnected(WebSocket websocket, WebSocketFrame
+ serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer) throws Exception {
+ Log.e(TAG, "Disconnected " + serverCloseFrame.getCloseReason() + " " + clientCloseFrame.getCloseReason() + " " + closedByServer);
+ }
+
+ @Override
+ public void onFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Frame");
+ }
+
+ @Override
+ public void onContinuationFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Continuation Frame");
+ }
+
+ @Override
+ public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Text Frame");
+ }
+
+ @Override
+ public void onBinaryFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Binary Frame");
+ }
+
+ @Override
+ public void onCloseFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Close Frame");
+ }
+
+ @Override
+ public void onPingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Ping Frame");
+ }
+
+ @Override
+ public void onPongFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Pong Frame");
+ }
+
+ @Override
+ public void onTextMessage(WebSocket websocket, byte[] data) throws Exception {
+
+ }
+
+ @Override
+ public void onBinaryMessage(WebSocket websocket, byte[] binary) throws Exception {
+ Log.i(TAG, "Binary Message");
+ }
+
+ @Override
+ public void onSendingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Sending Frame");
+ }
+
+ @Override
+ public void onFrameSent(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Frame sent");
+ }
+
+ @Override
+ public void onFrameUnsent(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Frame unsent");
+ }
+
+ @Override
+ public void onThreadCreated(WebSocket websocket, ThreadType threadType, Thread thread) throws
+ Exception {
+ Log.i(TAG, "Thread created");
+ }
+
+ @Override
+ public void onThreadStarted(WebSocket websocket, ThreadType threadType, Thread thread) throws
+ Exception {
+ Log.i(TAG, "Thread started");
+ }
+
+ @Override
+ public void onThreadStopping(WebSocket websocket, ThreadType threadType, Thread thread) throws
+ Exception {
+ Log.i(TAG, "Thread stopping");
+ }
+
+ @Override
+ public void onError(WebSocket websocket, WebSocketException cause) throws Exception {
+ Log.i(TAG, "Error!");
+ }
+
+ @Override
+ public void onFrameError(WebSocket websocket, WebSocketException cause, WebSocketFrame
+ frame) throws Exception {
+ Log.i(TAG, "Frame error!");
+ }
+
+ @Override
+ public void onMessageError(WebSocket websocket, WebSocketException
+ cause, List frames) throws Exception {
+ Log.i(TAG, "Message error! " + cause);
+ }
+
+ @Override
+ public void onMessageDecompressionError(WebSocket websocket, WebSocketException cause,
+ byte[] compressed) throws Exception {
+ Log.i(TAG, "Message decompression error!");
+ }
+
+ @Override
+ public void onTextMessageError(WebSocket websocket, WebSocketException cause, byte[] data) throws
+ Exception {
+ Log.i(TAG, "Text message error! " + cause);
+ }
+
+ @Override
+ public void onSendError(WebSocket websocket, WebSocketException cause, WebSocketFrame frame) throws
+ Exception {
+ Log.i(TAG, "Send error! " + cause);
+ }
+
+ @Override
+ public void onUnexpectedError(WebSocket websocket, WebSocketException cause) throws
+ Exception {
+ Log.i(TAG, "Unexpected error! " + cause);
+ }
+
+ @Override
+ public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception {
+ Log.e(TAG, "Handle callback error! " + cause);
+ }
+
+ @Override
+ public void onSendingHandshake(WebSocket websocket, String requestLine, List
+ headers) throws Exception {
+ Log.i(TAG, "Sending Handshake! Hello!");
+ }
+
+ private void pingMessageHandler() {
+ long initialDelay = 0L;
+ ScheduledThreadPoolExecutor executor =
+ new ScheduledThreadPoolExecutor(1);
+ executor.scheduleWithFixedDelay(() -> {
+ Map pingParams = new HashMap<>();
+ if (ID_PING.get() == -1) {
+ // First ping call
+ pingParams.put("interval", "5000");
+ }
+ ID_PING.set(sendJson(JsonConstants.PING_METHOD, pingParams));
+ }, initialDelay, PING_MESSAGE_INTERVAL, TimeUnit.SECONDS);
+ }
+
+ private String getWebSocketAddress(String openviduUrl) {
+ try {
+ URL url = new URL(openviduUrl);
+ if (url.getPort() > -1)
+ return "wss://" + url.getHost() + ":" + url.getPort() + "/openvidu";
+ return "wss://" + url.getHost() + "/openvidu";
+ } catch (MalformedURLException e) {
+ Log.e(TAG, "Wrong URL", e);
+ e.printStackTrace();
+ return "";
+ }
+ }
+
+ @Override
+ protected Void doInBackground(SessionActivity... sessionActivities) {
+ try {
+ WebSocketFactory factory = new WebSocketFactory();
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, trustManagers, new java.security.SecureRandom());
+ factory.setSSLContext(sslContext);
+ factory.setVerifyHostname(false);
+ websocket = factory.createSocket(getWebSocketAddress(openviduUrl));
+ websocket.addListener(this);
+ websocket.connect();
+ } catch (KeyManagementException | NoSuchAlgorithmException | IOException | WebSocketException e) {
+ Log.e("WebSocket error", e.getMessage());
+ Handler mainHandler = new Handler(activity.getMainLooper());
+ Runnable myRunnable = () -> {
+ Toast toast = Toast.makeText(activity, e.getMessage(), Toast.LENGTH_LONG);
+ toast.show();
+ activity.leaveSession();
+ };
+ mainHandler.post(myRunnable);
+ websocketCancelled = true;
+ }
+ return null;
+ }
+
+ @Override
+ protected void onProgressUpdate(Void... progress) {
+ Log.i(TAG, "PROGRESS " + Arrays.toString(progress));
+ }
+
+}
diff --git a/openvidu-androidx/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/openvidu-androidx/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..1f6bb2906
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-androidx/app/src/main/res/drawable/ic_launcher_background.xml b/openvidu-androidx/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..0d025f9bf
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-androidx/app/src/main/res/layout/activity_main.xml b/openvidu-androidx/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..811c8980c
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-androidx/app/src/main/res/layout/peer_video.xml b/openvidu-androidx/app/src/main/res/layout/peer_video.xml
new file mode 100644
index 000000000..a07f59983
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/layout/peer_video.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-androidx/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/openvidu-androidx/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..036d09bc5
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-androidx/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/openvidu-androidx/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..036d09bc5
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher.png b/openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..7d83c6b6d
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..0773cb6ec
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..b1c01714c
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher.png b/openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..6a023bc22
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..be99ae303
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..05ace99a8
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..e8cf85f3a
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..2b1a1da1e
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..3839c7d2d
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..0684ea4c5
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..4a14af0b4
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..6335ab37d
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..5e1fe43ee
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..67fe97baa
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..fff5700c7
Binary files /dev/null and b/openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/openvidu-androidx/app/src/main/res/values/colors.xml b/openvidu-androidx/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..69b22338c
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/openvidu-androidx/app/src/main/res/values/ic_launcher_background.xml b/openvidu-androidx/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 000000000..c5d5899fd
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/openvidu-androidx/app/src/main/res/values/strings.xml b/openvidu-androidx/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..41f134297
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/values/strings.xml
@@ -0,0 +1,30 @@
+
+ WebRTCExampleApp
+
+ We need your help
+ GIVE PERMISSIONS!
+
+ SHARED_PREFERENCES_MAIN
+ OPENVIDU_URL
+
+ Login
+ Join
+ Leave session
+ CANCEL
+
+ OPENVIDU URL
+ https://192.168.1.106:4443/
+
+ SECRET
+ MY_SECRET
+
+ Session Name
+ SessionA
+
+ Participant Name
+ Participant
+
+ We can not give you service without your permission
+ NO INTERNET CONNECTION, PLEASE CHECK YOUR CONNECTION
+ COULD NOT ESTABLISH THE CONNECTION, TRY AGAIN
+
diff --git a/openvidu-androidx/app/src/main/res/values/styles.xml b/openvidu-androidx/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..5885930df
--- /dev/null
+++ b/openvidu-androidx/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/openvidu-androidx/app/src/test/java/io/openvidu/openvidu_android/ExampleUnitTest.java b/openvidu-androidx/app/src/test/java/io/openvidu/openvidu_android/ExampleUnitTest.java
new file mode 100644
index 000000000..29253b34e
--- /dev/null
+++ b/openvidu-androidx/app/src/test/java/io/openvidu/openvidu_android/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package io.openvidu.openvidu_android;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/openvidu-androidx/build.gradle b/openvidu-androidx/build.gradle
new file mode 100644
index 000000000..0acabe96a
--- /dev/null
+++ b/openvidu-androidx/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.0.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/openvidu-androidx/gradle.properties b/openvidu-androidx/gradle.properties
new file mode 100644
index 000000000..199d16ede
--- /dev/null
+++ b/openvidu-androidx/gradle.properties
@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+
diff --git a/openvidu-androidx/gradle/wrapper/gradle-wrapper.jar b/openvidu-androidx/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..f6b961fd5
Binary files /dev/null and b/openvidu-androidx/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/openvidu-androidx/gradle/wrapper/gradle-wrapper.properties b/openvidu-androidx/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..38cd02337
--- /dev/null
+++ b/openvidu-androidx/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jun 24 16:32:07 CDT 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/openvidu-androidx/gradlew b/openvidu-androidx/gradlew
new file mode 100644
index 000000000..cccdd3d51
--- /dev/null
+++ b/openvidu-androidx/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/openvidu-androidx/gradlew.bat b/openvidu-androidx/gradlew.bat
new file mode 100644
index 000000000..f9553162f
--- /dev/null
+++ b/openvidu-androidx/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/openvidu-androidx/settings.gradle b/openvidu-androidx/settings.gradle
new file mode 100644
index 000000000..b1a1253cd
--- /dev/null
+++ b/openvidu-androidx/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name='OpenVidu Android'