mirror of
				https://github.com/pierre42100/ComunicAndroid
				synced 2025-11-04 11:34:06 +00:00 
			
		
		
		
	First call on Android device
This commit is contained in:
		
							
								
								
									
										2
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							@@ -29,7 +29,7 @@
 | 
			
		||||
      </value>
 | 
			
		||||
    </option>
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
 | 
			
		||||
  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
 | 
			
		||||
    <output url="file://$PROJECT_DIR$/build/classes" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="ProjectType">
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,11 @@ apply plugin: 'com.android.application'
 | 
			
		||||
android {
 | 
			
		||||
    compileSdkVersion 28
 | 
			
		||||
 | 
			
		||||
    compileOptions {
 | 
			
		||||
        sourceCompatibility 1.8
 | 
			
		||||
        targetCompatibility 1.8
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    defaultConfig {
 | 
			
		||||
        applicationId "org.communiquons.android.comunic.client"
 | 
			
		||||
        minSdkVersion 21
 | 
			
		||||
@@ -59,6 +64,8 @@ dependencies {
 | 
			
		||||
    implementation 'com.android.support:design:28.0.0-rc02'
 | 
			
		||||
    implementation 'com.android.support:preference-v7:28.0.0-rc02'
 | 
			
		||||
    implementation 'com.android.support:support-v4:28.0.0-rc02'
 | 
			
		||||
    implementation 'com.squareup.okhttp3:okhttp:3.12.1'
 | 
			
		||||
    implementation 'org.whispersystems:webrtc-android:M71'
 | 
			
		||||
    testImplementation 'junit:junit:4.12'
 | 
			
		||||
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
 | 
			
		||||
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,12 @@
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <!-- Video calls require camera and microphone -->
 | 
			
		||||
    <uses-permission android:name="android.permission.CAMERA" />
 | 
			
		||||
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <application
 | 
			
		||||
        android:allowBackup="true"
 | 
			
		||||
        android:fullBackupContent="@xml/backup_scheme"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										137
									
								
								app/src/main/java/org/appspot/apprtc/AppRTCClient.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								app/src/main/java/org/appspot/apprtc/AppRTCClient.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
/*
 | 
			
		||||
 *  Copyright 2013 The WebRTC Project Authors. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 *  Use of this source code is governed by a BSD-style license
 | 
			
		||||
 *  that can be found in the LICENSE file in the root of the source
 | 
			
		||||
 *  tree. An additional intellectual property rights grant can be found
 | 
			
		||||
 *  in the file PATENTS.  All contributing project authors may
 | 
			
		||||
 *  be found in the AUTHORS file in the root of the source tree.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.appspot.apprtc;
 | 
			
		||||
 | 
			
		||||
import org.webrtc.IceCandidate;
 | 
			
		||||
import org.webrtc.PeerConnection;
 | 
			
		||||
import org.webrtc.SessionDescription;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * AppRTCClient is the interface representing an AppRTC client.
 | 
			
		||||
 */
 | 
			
		||||
public interface AppRTCClient {
 | 
			
		||||
    /**
 | 
			
		||||
     * Struct holding the connection parameters of an AppRTC room.
 | 
			
		||||
     */
 | 
			
		||||
    class RoomConnectionParameters {
 | 
			
		||||
        public final String roomUrl;
 | 
			
		||||
        public final String roomId;
 | 
			
		||||
        public final boolean loopback;
 | 
			
		||||
        public final String urlParameters;
 | 
			
		||||
        public RoomConnectionParameters(
 | 
			
		||||
                String roomUrl, String roomId, boolean loopback, String urlParameters) {
 | 
			
		||||
            this.roomUrl = roomUrl;
 | 
			
		||||
            this.roomId = roomId;
 | 
			
		||||
            this.loopback = loopback;
 | 
			
		||||
            this.urlParameters = urlParameters;
 | 
			
		||||
        }
 | 
			
		||||
        public RoomConnectionParameters(String roomUrl, String roomId, boolean loopback) {
 | 
			
		||||
            this(roomUrl, roomId, loopback, null /* urlParameters */);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Asynchronously connect to an AppRTC room URL using supplied connection
 | 
			
		||||
     * parameters. Once connection is established onConnectedToRoom()
 | 
			
		||||
     * callback with room parameters is invoked.
 | 
			
		||||
     */
 | 
			
		||||
    void connectToRoom(RoomConnectionParameters connectionParameters);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send offer SDP to the other participant.
 | 
			
		||||
     */
 | 
			
		||||
    void sendOfferSdp(final SessionDescription sdp);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send answer SDP to the other participant.
 | 
			
		||||
     */
 | 
			
		||||
    void sendAnswerSdp(final SessionDescription sdp);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send Ice candidate to the other participant.
 | 
			
		||||
     */
 | 
			
		||||
    void sendLocalIceCandidate(final IceCandidate candidate);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send removed ICE candidates to the other participant.
 | 
			
		||||
     */
 | 
			
		||||
    void sendLocalIceCandidateRemovals(final IceCandidate[] candidates);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Disconnect from room.
 | 
			
		||||
     */
 | 
			
		||||
    void disconnectFromRoom();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Struct holding the signaling parameters of an AppRTC room.
 | 
			
		||||
     */
 | 
			
		||||
    class SignalingParameters {
 | 
			
		||||
        public final List<PeerConnection.IceServer> iceServers;
 | 
			
		||||
        public final boolean initiator;
 | 
			
		||||
        public final String clientId;
 | 
			
		||||
        public final String wssUrl;
 | 
			
		||||
        public final String wssPostUrl;
 | 
			
		||||
        public final SessionDescription offerSdp;
 | 
			
		||||
        public final List<IceCandidate> iceCandidates;
 | 
			
		||||
 | 
			
		||||
        public SignalingParameters(List<PeerConnection.IceServer> iceServers, boolean initiator,
 | 
			
		||||
                                   String clientId, String wssUrl, String wssPostUrl, SessionDescription offerSdp,
 | 
			
		||||
                                   List<IceCandidate> iceCandidates) {
 | 
			
		||||
            this.iceServers = iceServers;
 | 
			
		||||
            this.initiator = initiator;
 | 
			
		||||
            this.clientId = clientId;
 | 
			
		||||
            this.wssUrl = wssUrl;
 | 
			
		||||
            this.wssPostUrl = wssPostUrl;
 | 
			
		||||
            this.offerSdp = offerSdp;
 | 
			
		||||
            this.iceCandidates = iceCandidates;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Callback interface for messages delivered on signaling channel.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>Methods are guaranteed to be invoked on the UI thread of |activity|.
 | 
			
		||||
     */
 | 
			
		||||
    interface SignalingEvents {
 | 
			
		||||
        /**
 | 
			
		||||
         * Callback fired once the room's signaling parameters
 | 
			
		||||
         * SignalingParameters are extracted.
 | 
			
		||||
         */
 | 
			
		||||
        void onConnectedToRoom(final SignalingParameters params);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Callback fired once remote SDP is received.
 | 
			
		||||
         */
 | 
			
		||||
        void onRemoteDescription(final SessionDescription sdp);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Callback fired once remote Ice candidate is received.
 | 
			
		||||
         */
 | 
			
		||||
        void onRemoteIceCandidate(final IceCandidate candidate);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Callback fired once remote Ice candidate removals are received.
 | 
			
		||||
         */
 | 
			
		||||
        void onRemoteIceCandidatesRemoved(final IceCandidate[] candidates);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Callback fired once channel is closed.
 | 
			
		||||
         */
 | 
			
		||||
        void onChannelClose();
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Callback fired once channel error happened.
 | 
			
		||||
         */
 | 
			
		||||
        void onChannelError(final String description);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1428
									
								
								app/src/main/java/org/appspot/apprtc/PeerConnectionClient.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1428
									
								
								app/src/main/java/org/appspot/apprtc/PeerConnectionClient.java
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -0,0 +1,153 @@
 | 
			
		||||
/*
 | 
			
		||||
 *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
 | 
			
		||||
 *
 | 
			
		||||
 *  Use of this source code is governed by a BSD-style license
 | 
			
		||||
 *  that can be found in the LICENSE file in the root of the source
 | 
			
		||||
 *  tree. An additional intellectual property rights grant can be found
 | 
			
		||||
 *  in the file PATENTS.  All contributing project authors may
 | 
			
		||||
 *  be found in the AUTHORS file in the root of the source tree.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.appspot.apprtc;
 | 
			
		||||
 | 
			
		||||
import android.media.AudioFormat;
 | 
			
		||||
import android.os.Environment;
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.FileNotFoundException;
 | 
			
		||||
import java.io.FileOutputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.OutputStream;
 | 
			
		||||
import java.util.concurrent.ExecutorService;
 | 
			
		||||
import org.webrtc.audio.JavaAudioDeviceModule;
 | 
			
		||||
import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback;
 | 
			
		||||
import org.webrtc.voiceengine.WebRtcAudioRecord;
 | 
			
		||||
import org.webrtc.voiceengine.WebRtcAudioRecord.WebRtcAudioRecordSamplesReadyCallback;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implements the AudioRecordSamplesReadyCallback interface and writes
 | 
			
		||||
 * recorded raw audio samples to an output file.
 | 
			
		||||
 */
 | 
			
		||||
public class RecordedAudioToFileController
 | 
			
		||||
        implements SamplesReadyCallback, WebRtcAudioRecordSamplesReadyCallback {
 | 
			
		||||
    private static final String TAG = "RecordedAudioToFile";
 | 
			
		||||
    private static final long MAX_FILE_SIZE_IN_BYTES = 58348800L;
 | 
			
		||||
 | 
			
		||||
    private final Object lock = new Object();
 | 
			
		||||
    private final ExecutorService executor;
 | 
			
		||||
    @Nullable private OutputStream rawAudioFileOutputStream;
 | 
			
		||||
    private boolean isRunning;
 | 
			
		||||
    private long fileSizeInBytes;
 | 
			
		||||
 | 
			
		||||
    public RecordedAudioToFileController(ExecutorService executor) {
 | 
			
		||||
        Log.d(TAG, "ctor");
 | 
			
		||||
        this.executor = executor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Should be called on the same executor thread as the one provided at
 | 
			
		||||
     * construction.
 | 
			
		||||
     */
 | 
			
		||||
    public boolean start() {
 | 
			
		||||
        Log.d(TAG, "start");
 | 
			
		||||
        if (!isExternalStorageWritable()) {
 | 
			
		||||
            Log.e(TAG, "Writing to external media is not possible");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        synchronized (lock) {
 | 
			
		||||
            isRunning = true;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Should be called on the same executor thread as the one provided at
 | 
			
		||||
     * construction.
 | 
			
		||||
     */
 | 
			
		||||
    public void stop() {
 | 
			
		||||
        Log.d(TAG, "stop");
 | 
			
		||||
        synchronized (lock) {
 | 
			
		||||
            isRunning = false;
 | 
			
		||||
            if (rawAudioFileOutputStream != null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    rawAudioFileOutputStream.close();
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    Log.e(TAG, "Failed to close file with saved input audio: " + e);
 | 
			
		||||
                }
 | 
			
		||||
                rawAudioFileOutputStream = null;
 | 
			
		||||
            }
 | 
			
		||||
            fileSizeInBytes = 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Checks if external storage is available for read and write.
 | 
			
		||||
    private boolean isExternalStorageWritable() {
 | 
			
		||||
        String state = Environment.getExternalStorageState();
 | 
			
		||||
        if (Environment.MEDIA_MOUNTED.equals(state)) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Utilizes audio parameters to create a file name which contains sufficient
 | 
			
		||||
    // information so that the file can be played using an external file player.
 | 
			
		||||
    // Example: /sdcard/recorded_audio_16bits_48000Hz_mono.pcm.
 | 
			
		||||
    private void openRawAudioOutputFile(int sampleRate, int channelCount) {
 | 
			
		||||
        final String fileName = Environment.getExternalStorageDirectory().getPath() + File.separator
 | 
			
		||||
                + "recorded_audio_16bits_" + String.valueOf(sampleRate) + "Hz"
 | 
			
		||||
                + ((channelCount == 1) ? "_mono" : "_stereo") + ".pcm";
 | 
			
		||||
        final File outputFile = new File(fileName);
 | 
			
		||||
        try {
 | 
			
		||||
            rawAudioFileOutputStream = new FileOutputStream(outputFile);
 | 
			
		||||
        } catch (FileNotFoundException e) {
 | 
			
		||||
            Log.e(TAG, "Failed to open audio output file: " + e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(TAG, "Opened file for recording: " + fileName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Called when new audio samples are ready.
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onWebRtcAudioRecordSamplesReady(WebRtcAudioRecord.AudioSamples samples) {
 | 
			
		||||
        onWebRtcAudioRecordSamplesReady(new JavaAudioDeviceModule.AudioSamples(samples.getAudioFormat(),
 | 
			
		||||
                samples.getChannelCount(), samples.getSampleRate(), samples.getData()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Called when new audio samples are ready.
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onWebRtcAudioRecordSamplesReady(JavaAudioDeviceModule.AudioSamples samples) {
 | 
			
		||||
        // The native audio layer on Android should use 16-bit PCM format.
 | 
			
		||||
        if (samples.getAudioFormat() != AudioFormat.ENCODING_PCM_16BIT) {
 | 
			
		||||
            Log.e(TAG, "Invalid audio format");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        synchronized (lock) {
 | 
			
		||||
            // Abort early if stop() has been called.
 | 
			
		||||
            if (!isRunning) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            // Open a new file for the first callback only since it allows us to add audio parameters to
 | 
			
		||||
            // the file name.
 | 
			
		||||
            if (rawAudioFileOutputStream == null) {
 | 
			
		||||
                openRawAudioOutputFile(samples.getSampleRate(), samples.getChannelCount());
 | 
			
		||||
                fileSizeInBytes = 0;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // Append the recorded 16-bit audio samples to the open output file.
 | 
			
		||||
        executor.execute(() -> {
 | 
			
		||||
            if (rawAudioFileOutputStream != null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    // Set a limit on max file size. 58348800 bytes corresponds to
 | 
			
		||||
                    // approximately 10 minutes of recording in mono at 48kHz.
 | 
			
		||||
                    if (fileSizeInBytes < MAX_FILE_SIZE_IN_BYTES) {
 | 
			
		||||
                        // Writes samples.getData().length bytes to output stream.
 | 
			
		||||
                        rawAudioFileOutputStream.write(samples.getData());
 | 
			
		||||
                        fileSizeInBytes += samples.getData().length;
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    Log.e(TAG, "Failed to write audio to file: " + e.getMessage());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								app/src/main/java/org/appspot/apprtc/RtcEventLog.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/src/main/java/org/appspot/apprtc/RtcEventLog.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
/*
 | 
			
		||||
 *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
 | 
			
		||||
 *
 | 
			
		||||
 *  Use of this source code is governed by a BSD-style license
 | 
			
		||||
 *  that can be found in the LICENSE file in the root of the source
 | 
			
		||||
 *  tree. An additional intellectual property rights grant can be found
 | 
			
		||||
 *  in the file PATENTS.  All contributing project authors may
 | 
			
		||||
 *  be found in the AUTHORS file in the root of the source tree.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.appspot.apprtc;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.os.ParcelFileDescriptor;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import org.webrtc.PeerConnection;
 | 
			
		||||
 | 
			
		||||
public class RtcEventLog {
 | 
			
		||||
    private static final String TAG = "RtcEventLog";
 | 
			
		||||
    private static final int OUTPUT_FILE_MAX_BYTES = 10_000_000;
 | 
			
		||||
    private final PeerConnection peerConnection;
 | 
			
		||||
    private RtcEventLogState state = RtcEventLogState.INACTIVE;
 | 
			
		||||
 | 
			
		||||
    enum RtcEventLogState {
 | 
			
		||||
        INACTIVE,
 | 
			
		||||
        STARTED,
 | 
			
		||||
        STOPPED,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public RtcEventLog(PeerConnection peerConnection) {
 | 
			
		||||
        if (peerConnection == null) {
 | 
			
		||||
            throw new NullPointerException("The peer connection is null.");
 | 
			
		||||
        }
 | 
			
		||||
        this.peerConnection = peerConnection;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void start(final File outputFile) {
 | 
			
		||||
        if (state == RtcEventLogState.STARTED) {
 | 
			
		||||
            Log.e(TAG, "RtcEventLog has already started.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        final ParcelFileDescriptor fileDescriptor;
 | 
			
		||||
        try {
 | 
			
		||||
            fileDescriptor = ParcelFileDescriptor.open(outputFile,
 | 
			
		||||
                    ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE
 | 
			
		||||
                            | ParcelFileDescriptor.MODE_TRUNCATE);
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            Log.e(TAG, "Failed to create a new file", e);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Passes ownership of the file to WebRTC.
 | 
			
		||||
        boolean success =
 | 
			
		||||
                peerConnection.startRtcEventLog(fileDescriptor.detachFd(), OUTPUT_FILE_MAX_BYTES);
 | 
			
		||||
        if (!success) {
 | 
			
		||||
            Log.e(TAG, "Failed to start RTC event log.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        state = RtcEventLogState.STARTED;
 | 
			
		||||
        Log.d(TAG, "RtcEventLog started.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void stop() {
 | 
			
		||||
        if (state != RtcEventLogState.STARTED) {
 | 
			
		||||
            Log.e(TAG, "RtcEventLog was not started.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        peerConnection.stopRtcEventLog();
 | 
			
		||||
        state = RtcEventLogState.STOPPED;
 | 
			
		||||
        Log.d(TAG, "RtcEventLog stopped.");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,6 +15,9 @@ import org.communiquons.android.comunic.client.data.models.NextPendingCallInform
 | 
			
		||||
import org.json.JSONArray;
 | 
			
		||||
import org.json.JSONException;
 | 
			
		||||
import org.json.JSONObject;
 | 
			
		||||
import org.webrtc.PeerConnection;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calls helper
 | 
			
		||||
@@ -90,6 +93,30 @@ public class CallsHelper extends BaseHelper {
 | 
			
		||||
        return mCallsConfiguration != null && mCallsConfiguration.isEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the list of STUN and TURN servers
 | 
			
		||||
     *
 | 
			
		||||
     * @return List of STUN and TURN servers available for Comunic
 | 
			
		||||
     */
 | 
			
		||||
    public static ArrayList<PeerConnection.IceServer> GetPeerServers(){
 | 
			
		||||
 | 
			
		||||
        ArrayList<PeerConnection.IceServer> servers = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        //Stun server
 | 
			
		||||
        servers.add(PeerConnection.IceServer.builder(
 | 
			
		||||
                mCallsConfiguration.getStunServer()).createIceServer());
 | 
			
		||||
 | 
			
		||||
        //TURN server
 | 
			
		||||
        servers.add(PeerConnection.IceServer
 | 
			
		||||
                .builder(mCallsConfiguration.getTurnServer())
 | 
			
		||||
                .setUsername(mCallsConfiguration.getTurnUsername())
 | 
			
		||||
                .setPassword(mCallsConfiguration.getTurnPassword())
 | 
			
		||||
                .createIceServer()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return servers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a call for a conversation, returns information about this call then
 | 
			
		||||
     *
 | 
			
		||||
@@ -151,6 +178,26 @@ public class CallsHelper extends BaseHelper {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get information about a call
 | 
			
		||||
     *
 | 
			
		||||
     * @param callID Target call ID
 | 
			
		||||
     * @return Information about the call / null in case of failure
 | 
			
		||||
     */
 | 
			
		||||
    public CallInformation getInfo(int callID){
 | 
			
		||||
 | 
			
		||||
        APIRequest request = new APIRequest(getContext(), "calls/getInfo");
 | 
			
		||||
        request.addInt("call_id", callID);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            return JSONObjectToCallInformation(request.exec().getJSONObject(), null);
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to get and return call information
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package org.communiquons.android.comunic.client.data.models;
 | 
			
		||||
 | 
			
		||||
import android.support.annotation.NonNull;
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
 | 
			
		||||
@@ -75,6 +76,36 @@ public class CallInformation {
 | 
			
		||||
        this.members = members;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find a member by user ID
 | 
			
		||||
     *
 | 
			
		||||
     * @param userID The ID of the user to search
 | 
			
		||||
     * @return Information about the target user
 | 
			
		||||
     */
 | 
			
		||||
    public CallMember findMember(int userID){
 | 
			
		||||
        for(CallMember member : members)
 | 
			
		||||
            if(member.getUserID() == userID)
 | 
			
		||||
                return member;
 | 
			
		||||
 | 
			
		||||
        throw new RuntimeException("Specified user was not found in the conversation!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find a member by call ID
 | 
			
		||||
     *
 | 
			
		||||
     * @param userCallID The ID of the target user
 | 
			
		||||
     * @return Information about the user / null in case of failure
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public CallMember findMember(String userCallID){
 | 
			
		||||
        for(CallMember member : members)
 | 
			
		||||
            if(member.getUserCallID().equals(userCallID))
 | 
			
		||||
                return member;
 | 
			
		||||
 | 
			
		||||
        throw new RuntimeException("Specified user was not found in the conversation!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public String getCallName() {
 | 
			
		||||
        return callName;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ public class CallsConfiguration {
 | 
			
		||||
        this.signalServerPort = signalServerPort;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isSignalSererSecure() {
 | 
			
		||||
    public boolean isSignalServerSecure() {
 | 
			
		||||
        return isSignalSererSecure;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,46 @@
 | 
			
		||||
package org.communiquons.android.comunic.client.ui.activities;
 | 
			
		||||
 | 
			
		||||
import android.Manifest;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.os.AsyncTask;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
import android.support.v4.app.ActivityCompat;
 | 
			
		||||
import android.support.v4.content.ContextCompat;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.widget.ProgressBar;
 | 
			
		||||
import android.widget.Toast;
 | 
			
		||||
 | 
			
		||||
import org.appspot.apprtc.AppRTCClient;
 | 
			
		||||
import org.appspot.apprtc.PeerConnectionClient;
 | 
			
		||||
import org.communiquons.android.comunic.client.R;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.enums.MemberCallStatus;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.helpers.CallsHelper;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.models.CallInformation;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.models.CallMember;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.models.CallResponse;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.models.CallsConfiguration;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.utils.AccountUtils;
 | 
			
		||||
import org.communiquons.android.comunic.client.ui.arrays.CallPeersConnectionsList;
 | 
			
		||||
import org.communiquons.android.comunic.client.ui.asynctasks.GetCallInformationTask;
 | 
			
		||||
import org.communiquons.android.comunic.client.ui.asynctasks.RespondToCallTask;
 | 
			
		||||
import org.communiquons.android.comunic.client.ui.models.CallPeerConnection;
 | 
			
		||||
import org.communiquons.android.comunic.client.ui.receivers.PendingCallsBroadcastReceiver;
 | 
			
		||||
import org.communiquons.signalexchangerclient.SignalExchangerCallback;
 | 
			
		||||
import org.communiquons.signalexchangerclient.SignalExchangerClient;
 | 
			
		||||
import org.communiquons.signalexchangerclient.SignalExchangerInitConfig;
 | 
			
		||||
import org.webrtc.Camera1Enumerator;
 | 
			
		||||
import org.webrtc.CameraEnumerator;
 | 
			
		||||
import org.webrtc.EglBase;
 | 
			
		||||
import org.webrtc.IceCandidate;
 | 
			
		||||
import org.webrtc.Logging;
 | 
			
		||||
import org.webrtc.PeerConnectionFactory;
 | 
			
		||||
import org.webrtc.SessionDescription;
 | 
			
		||||
import org.webrtc.StatsReport;
 | 
			
		||||
import org.webrtc.SurfaceViewRenderer;
 | 
			
		||||
import org.webrtc.VideoCapturer;
 | 
			
		||||
import org.webrtc.VideoFrame;
 | 
			
		||||
import org.webrtc.VideoSink;
 | 
			
		||||
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
@@ -13,21 +49,89 @@ import java.util.Objects;
 | 
			
		||||
 *
 | 
			
		||||
 * @author Pierre HUBERT
 | 
			
		||||
 */
 | 
			
		||||
public class CallActivity extends BaseActivity {
 | 
			
		||||
public class CallActivity extends BaseActivity implements SignalExchangerCallback {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Debug tag
 | 
			
		||||
     */
 | 
			
		||||
    private static final String TAG = CallActivity.class.getSimpleName();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mandatory argument that includes call id
 | 
			
		||||
     */
 | 
			
		||||
    public static final String ARGUMENT_CALL_ID = "call_id";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Permissions requests codes
 | 
			
		||||
     */
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh call information thread
 | 
			
		||||
     */
 | 
			
		||||
    private RefreshCallInformation mRefreshCallInformation = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Current call ID and information
 | 
			
		||||
     */
 | 
			
		||||
    private int mCallID = -1;
 | 
			
		||||
    private CallInformation mCallInformation = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Signal exchanger client
 | 
			
		||||
     */
 | 
			
		||||
    private SignalExchangerClient mSignalExchangerClient = null;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Connections list
 | 
			
		||||
     */
 | 
			
		||||
    private CallPeersConnectionsList mList = new CallPeersConnectionsList();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * WebRTC attributes
 | 
			
		||||
     */
 | 
			
		||||
    private EglBase rootEglBase;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Views
 | 
			
		||||
     */
 | 
			
		||||
    private ProgressBar mProgressBar;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
        setContentView(R.layout.activity_call);
 | 
			
		||||
 | 
			
		||||
        //Hide call bar
 | 
			
		||||
        Objects.requireNonNull(getSupportActionBar()).hide();
 | 
			
		||||
        mCallID = getIntent().getIntExtra(ARGUMENT_CALL_ID, 0);
 | 
			
		||||
 | 
			
		||||
        //Get views
 | 
			
		||||
        initViews();
 | 
			
		||||
        initVideos();
 | 
			
		||||
 | 
			
		||||
        //Mark the call as accepted
 | 
			
		||||
        RespondToCallTask respondToCallTask = new RespondToCallTask(this);
 | 
			
		||||
        respondToCallTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
 | 
			
		||||
                new CallResponse(mCallID, true));
 | 
			
		||||
        getTasksManager().addTask(respondToCallTask);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onStart() {
 | 
			
		||||
        super.onStart();
 | 
			
		||||
 | 
			
		||||
        //Refresh at a regular interval information about the call
 | 
			
		||||
        mRefreshCallInformation = new RefreshCallInformation();
 | 
			
		||||
        mRefreshCallInformation.start();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -37,7 +141,507 @@ public class CallActivity extends BaseActivity {
 | 
			
		||||
        //Hide call notifications
 | 
			
		||||
        PendingCallsBroadcastReceiver.RemoveCallNotification(this);
 | 
			
		||||
 | 
			
		||||
        ((TextView)findViewById(R.id.call_id)).setText(
 | 
			
		||||
                "Call " + getIntent().getExtras().getInt(ARGUMENT_CALL_ID));
 | 
			
		||||
        //Make sure we have access to user camera and microphone
 | 
			
		||||
        askForPermissions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onStop() {
 | 
			
		||||
        super.onStop();
 | 
			
		||||
        mRefreshCallInformation.interrupt();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Request access to user camera and microphone devices
 | 
			
		||||
     *
 | 
			
		||||
     * Based on https://github.com/sergiopaniego/WebRTCAndroidExample
 | 
			
		||||
     */
 | 
			
		||||
    private void askForPermissions() {
 | 
			
		||||
        if ((ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
 | 
			
		||||
                != PackageManager.PERMISSION_GRANTED) &&
 | 
			
		||||
                (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
 | 
			
		||||
                        != PackageManager.PERMISSION_GRANTED)) {
 | 
			
		||||
            ActivityCompat.requestPermissions(this,
 | 
			
		||||
                    new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},
 | 
			
		||||
                    MY_PERMISSIONS_REQUEST);
 | 
			
		||||
        } else if (ContextCompat.checkSelfPermission(this,
 | 
			
		||||
                Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
 | 
			
		||||
            ActivityCompat.requestPermissions(this,
 | 
			
		||||
                    new String[]{Manifest.permission.RECORD_AUDIO},
 | 
			
		||||
                    MY_PERMISSIONS_REQUEST_RECORD_AUDIO);
 | 
			
		||||
 | 
			
		||||
        } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
 | 
			
		||||
                != PackageManager.PERMISSION_GRANTED) {
 | 
			
		||||
            ActivityCompat.requestPermissions(this,
 | 
			
		||||
                    new String[]{Manifest.permission.CAMERA},
 | 
			
		||||
                    MY_PERMISSIONS_REQUEST_CAMERA);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get views
 | 
			
		||||
     */
 | 
			
		||||
    private void initViews(){
 | 
			
		||||
 | 
			
		||||
        mProgressBar = findViewById(R.id.progressBar);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private void initVideos(){
 | 
			
		||||
 | 
			
		||||
        rootEglBase = EglBase.create();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh call information
 | 
			
		||||
     */
 | 
			
		||||
    private void getCallInformation(){
 | 
			
		||||
 | 
			
		||||
        GetCallInformationTask getCallInformationTask = new GetCallInformationTask(this);
 | 
			
		||||
        getCallInformationTask.setOnPostExecuteListener(this::onGotCallInformation);
 | 
			
		||||
        getCallInformationTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mCallID);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Once we have got information about the call
 | 
			
		||||
     *
 | 
			
		||||
     * @param info Information about the call
 | 
			
		||||
     */
 | 
			
		||||
    private void onGotCallInformation(@Nullable CallInformation info){
 | 
			
		||||
 | 
			
		||||
        if(info == null){
 | 
			
		||||
            Toast.makeText(this, R.string.err_get_call_info, Toast.LENGTH_SHORT).show();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setTitle(info.getCallName());
 | 
			
		||||
        mCallInformation = info;
 | 
			
		||||
 | 
			
		||||
        //Check if everyone left the conversation
 | 
			
		||||
        if(mCallInformation.hasAllMembersLeftCallExcept(AccountUtils.getID(this))){
 | 
			
		||||
            Toast.makeText(this, R.string.notice_call_terminated, Toast.LENGTH_SHORT).show();
 | 
			
		||||
            finish();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Connect to signaling server
 | 
			
		||||
        if(mSignalExchangerClient == null){
 | 
			
		||||
            initializeSignalClient();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Check connection is establish
 | 
			
		||||
        if(!mSignalExchangerClient.isConnected())
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        processClientsConnections();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private void initializeSignalClient(){
 | 
			
		||||
 | 
			
		||||
        CallsConfiguration callsConfiguration = CallsHelper.GetCallsConfiguration();
 | 
			
		||||
 | 
			
		||||
        assert callsConfiguration != null;
 | 
			
		||||
        mSignalExchangerClient = new SignalExchangerClient(new SignalExchangerInitConfig(
 | 
			
		||||
                callsConfiguration.getSignalServerName(),
 | 
			
		||||
                callsConfiguration.getSignalServerPort(),
 | 
			
		||||
                mCallInformation.findMember(AccountUtils.getID(this)).getUserCallID(),
 | 
			
		||||
                callsConfiguration.isSignalServerSecure()
 | 
			
		||||
        ), this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private void processClientsConnections(){
 | 
			
		||||
        //Process each peer connection
 | 
			
		||||
        for(CallMember member : mCallInformation.getMembers())
 | 
			
		||||
            processClientConnection(member);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void processClientConnection(CallMember member){
 | 
			
		||||
 | 
			
		||||
        //Skip current user
 | 
			
		||||
        if(member.getUserID() == AccountUtils.getID(this))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        //Check if the member left the conversation
 | 
			
		||||
        if(member.getStatus() != MemberCallStatus.ACCEPTED){
 | 
			
		||||
            disconnectFromPeer(member);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(mList.find(member) == null && member.getUserID() > AccountUtils.getID(this)) {
 | 
			
		||||
            createPeerConnection(member, false);
 | 
			
		||||
            mSignalExchangerClient.sendReadyMessage(member.getUserCallID());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if(mList.find(member) != null)
 | 
			
		||||
            Objects.requireNonNull(mList.find(member)).setMember(member);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create the peer connection for a specific call member
 | 
			
		||||
     *
 | 
			
		||||
     * @param member Target member
 | 
			
		||||
     * @param isInitiator Specify whether if we should send the offer or not to this user
 | 
			
		||||
     */
 | 
			
		||||
    private void createPeerConnection(CallMember member, boolean isInitiator){
 | 
			
		||||
 | 
			
		||||
        Log.v(TAG, "Create peer connection for connection with user " + member.getUserID());
 | 
			
		||||
 | 
			
		||||
        CallPeerConnection callPeer = new CallPeerConnection(member);
 | 
			
		||||
        mList.add(callPeer);
 | 
			
		||||
 | 
			
		||||
        //Create peer connection
 | 
			
		||||
        PeerConnectionClient peerConnectionClient = new PeerConnectionClient(
 | 
			
		||||
                getApplicationContext(),
 | 
			
		||||
                rootEglBase,
 | 
			
		||||
                new PeerConnectionClient.PeerConnectionParameters(
 | 
			
		||||
                        true,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        0,
 | 
			
		||||
                        0,
 | 
			
		||||
                        0,
 | 
			
		||||
                        0,
 | 
			
		||||
                        "",
 | 
			
		||||
                        true,
 | 
			
		||||
                        false,
 | 
			
		||||
                        0,
 | 
			
		||||
                        null,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        false,
 | 
			
		||||
                        null
 | 
			
		||||
                ),
 | 
			
		||||
                new PeerConnectionEvents(callPeer)
 | 
			
		||||
        );
 | 
			
		||||
        callPeer.setPeerConnectionClient(peerConnectionClient);
 | 
			
		||||
 | 
			
		||||
        PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
 | 
			
		||||
        peerConnectionClient.createPeerConnectionFactory(options);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //Signaling parameters
 | 
			
		||||
        AppRTCClient.SignalingParameters parameters = new AppRTCClient.SignalingParameters(
 | 
			
		||||
                CallsHelper.GetPeerServers(), isInitiator, null,
 | 
			
		||||
                null, null, null, null
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //Initialize video view
 | 
			
		||||
        SurfaceViewRenderer localView = new SurfaceViewRenderer(this);
 | 
			
		||||
        localView.init(rootEglBase.getEglBaseContext(), null);
 | 
			
		||||
        localView.setZOrderMediaOverlay(true);
 | 
			
		||||
        callPeer.setLocalVideoView(localView);
 | 
			
		||||
 | 
			
		||||
        SurfaceViewRenderer remoteView = new SurfaceViewRenderer(this);
 | 
			
		||||
        remoteView.init(rootEglBase.getEglBaseContext(), null);
 | 
			
		||||
        remoteView.setZOrderMediaOverlay(false);
 | 
			
		||||
        callPeer.setRemoteViewView(remoteView);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        ProxyVideoSink localProxyVideoSink = new ProxyVideoSink();
 | 
			
		||||
        localProxyVideoSink.setTarget(callPeer.getLocalVideoView());
 | 
			
		||||
 | 
			
		||||
        ProxyVideoSink remoteProxyRenderer = new ProxyVideoSink();
 | 
			
		||||
        remoteProxyRenderer.setTarget(callPeer.getRemoteViewView());
 | 
			
		||||
        callPeer.getRemoteSinks().add(remoteProxyRenderer);
 | 
			
		||||
 | 
			
		||||
        //Start connection
 | 
			
		||||
        peerConnectionClient.createPeerConnection(
 | 
			
		||||
                localProxyVideoSink,
 | 
			
		||||
                callPeer.getRemoteSinks(),
 | 
			
		||||
                createCameraCapturer(new Camera1Enumerator(false)),
 | 
			
		||||
                parameters
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if(isInitiator)
 | 
			
		||||
            peerConnectionClient.createOffer();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Disconnect from a specific peer
 | 
			
		||||
     *
 | 
			
		||||
     * @param member Information about related call member
 | 
			
		||||
     */
 | 
			
		||||
    private void disconnectFromPeer(CallMember member){
 | 
			
		||||
 | 
			
		||||
        CallPeerConnection callPeer = mList.find(member);
 | 
			
		||||
        if(callPeer == null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        callPeer.getPeerConnectionClient().close();
 | 
			
		||||
 | 
			
		||||
        mList.remove(callPeer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //Based on https://github.com/vivek1794/webrtc-android-codelab
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private VideoCapturer createCameraCapturer(CameraEnumerator enumerator){
 | 
			
		||||
        final String[] deviceNames = enumerator.getDeviceNames();
 | 
			
		||||
 | 
			
		||||
        // First, try to find front facing camera
 | 
			
		||||
        Logging.d(TAG, "Looking for front facing cameras.");
 | 
			
		||||
        for (String deviceName : deviceNames) {
 | 
			
		||||
            if (enumerator.isFrontFacing(deviceName)) {
 | 
			
		||||
                Logging.d(TAG, "Creating front facing camera capturer.");
 | 
			
		||||
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
 | 
			
		||||
 | 
			
		||||
                if (videoCapturer != null) {
 | 
			
		||||
                    return videoCapturer;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Front facing camera not found, try something else
 | 
			
		||||
        Logging.d(TAG, "Looking for other cameras.");
 | 
			
		||||
        for (String deviceName : deviceNames) {
 | 
			
		||||
            if (!enumerator.isFrontFacing(deviceName)) {
 | 
			
		||||
                Logging.d(TAG, "Creating other camera capturer.");
 | 
			
		||||
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
 | 
			
		||||
 | 
			
		||||
                if (videoCapturer != null) {
 | 
			
		||||
                    return videoCapturer;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSignalServerError(String msg, @Nullable Throwable t) {
 | 
			
		||||
        runOnUiThread(() -> Toast.makeText(this,
 | 
			
		||||
                R.string.err_connect_signaling_server, Toast.LENGTH_SHORT).show());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onConnectedToSignalingServer() {
 | 
			
		||||
        runOnUiThread(this::processClientsConnections);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onReadyMessageCallback(String target_id, int number_targets) {
 | 
			
		||||
        Log.e(TAG, "Send ready message callback");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onReadyMessage(String source_id) {
 | 
			
		||||
 | 
			
		||||
        runOnUiThread(() -> {
 | 
			
		||||
 | 
			
		||||
            //Ignore message if a connection has already been established
 | 
			
		||||
            if (mList.findByCallID(source_id) != null) {
 | 
			
		||||
                Log.e(TAG, "Ignored ready message from " + source_id + " because a connection has already be made!");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            CallMember member = mCallInformation.findMember(source_id);
 | 
			
		||||
 | 
			
		||||
            if (member == null) {
 | 
			
		||||
                Log.e(TAG, source_id + " sent a ready message but it does not belong to the conversation!");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Log.v(TAG, source_id + " informed it is ready to establish connection.");
 | 
			
		||||
            createPeerConnection(member, true);
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSignal(String source_id, String signal) {
 | 
			
		||||
        Log.e(TAG, "Received new signal from " + source_id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSendSignalCallback(int number_targets) {
 | 
			
		||||
        Log.e(TAG, "Send signal callback, number of targets: " + number_targets);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void gotRemoteIceCandidate(String source_id, IceCandidate iceCandidate) {
 | 
			
		||||
 | 
			
		||||
        runOnUiThread(() -> {
 | 
			
		||||
 | 
			
		||||
            CallPeerConnection connection = mList.findByCallID(source_id);
 | 
			
		||||
            if(connection == null) {
 | 
			
		||||
                Log.e(TAG, "Dropped ICE candidate from " + source_id +  " no peer connection was ready to receive it!");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            connection.getPeerConnectionClient().addRemoteIceCandidate(iceCandidate);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void gotRemoteSessionDescription(String source_id, SessionDescription sessionDescription) {
 | 
			
		||||
 | 
			
		||||
        runOnUiThread(() -> {
 | 
			
		||||
 | 
			
		||||
            CallPeerConnection connection = mList.findByCallID(source_id);
 | 
			
		||||
            if(connection == null) {
 | 
			
		||||
                Log.e(TAG, "Dropped session description from " + source_id +  " no peer connection was ready to receive it!");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            connection.getPeerConnectionClient().setRemoteDescription(sessionDescription);
 | 
			
		||||
            connection.getPeerConnectionClient().createAnswer();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Class used to received events that comes from a connection
 | 
			
		||||
     */
 | 
			
		||||
    private class PeerConnectionEvents implements PeerConnectionClient.PeerConnectionEvents {
 | 
			
		||||
 | 
			
		||||
        private CallPeerConnection connection;
 | 
			
		||||
 | 
			
		||||
        PeerConnectionEvents(CallPeerConnection connection) {
 | 
			
		||||
            this.connection = connection;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onLocalDescription(SessionDescription sdp) {
 | 
			
		||||
            Log.v(TAG, "Got a new local description");
 | 
			
		||||
            runOnUiThread(() ->
 | 
			
		||||
                    mSignalExchangerClient.sendSessionDescription(
 | 
			
		||||
                            connection.getMember().getUserCallID(), sdp));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onIceCandidate(IceCandidate candidate) {
 | 
			
		||||
            Log.v(TAG, "Got a new ICE candidate");
 | 
			
		||||
            runOnUiThread(() -> mSignalExchangerClient.sendIceCandidate(
 | 
			
		||||
                    connection.getMember().getUserCallID(),
 | 
			
		||||
                    candidate));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onIceCandidatesRemoved(IceCandidate[] candidates) {
 | 
			
		||||
            Log.v(TAG, "Some ice candidates removed with peer  " +
 | 
			
		||||
                    connection.getMember().getUserID());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onIceConnected() {
 | 
			
		||||
            Log.v(TAG, "Ice connected with peer  " +
 | 
			
		||||
                    connection.getMember().getUserID());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onIceDisconnected() {
 | 
			
		||||
            Log.v(TAG, "Ice disconnected from peer  " +
 | 
			
		||||
                    connection.getMember().getUserID());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onConnected() {
 | 
			
		||||
            Log.v(TAG, "Connected to peer  " +
 | 
			
		||||
                    connection.getMember().getUserID());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onDisconnected() {
 | 
			
		||||
            Log.v(TAG, "Disconnected from peer  " +
 | 
			
		||||
                    connection.getMember().getUserID());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onPeerConnectionClosed() {
 | 
			
		||||
            Log.v(TAG, "Connection close from user " +
 | 
			
		||||
                    connection.getMember().getUserID());
 | 
			
		||||
            runOnUiThread(() -> disconnectFromPeer(connection.getMember()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onPeerConnectionStatsReady(StatsReport[] reports) {
 | 
			
		||||
            Log.v(TAG, "Stats ready for peer connection with " +
 | 
			
		||||
                    connection.getMember().getUserID());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onPeerConnectionError(String description) {
 | 
			
		||||
            Log.e(TAG, "Peer connection error with " +
 | 
			
		||||
                    connection.getMember().getUserID() + " " + description);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh call information thread
 | 
			
		||||
     */
 | 
			
		||||
    private class RefreshCallInformation extends Thread {
 | 
			
		||||
 | 
			
		||||
        private final Object o = new Object();
 | 
			
		||||
        private boolean stop = false;
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void run() {
 | 
			
		||||
            super.run();
 | 
			
		||||
 | 
			
		||||
            synchronized (o){
 | 
			
		||||
 | 
			
		||||
                while(!stop) {
 | 
			
		||||
 | 
			
		||||
                    runOnUiThread(CallActivity.this::getCallInformation);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        o.wait((long) (1.5 * 1000));
 | 
			
		||||
                    } catch (InterruptedException e) {
 | 
			
		||||
                        e.printStackTrace();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void interrupt(){
 | 
			
		||||
            stop = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * I don't know why, but this is an absolute requirement ! (to show videos)
 | 
			
		||||
     */
 | 
			
		||||
    private static class ProxyVideoSink implements VideoSink {
 | 
			
		||||
        private VideoSink target;
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        synchronized public void onFrame(VideoFrame frame) {
 | 
			
		||||
            if (target == null) {
 | 
			
		||||
                Logging.d(TAG, "Dropping frame in proxy because target is null.");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            target.onFrame(frame);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        synchronized public void setTarget(VideoSink target) {
 | 
			
		||||
            this.target = target;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ import android.widget.Toast;
 | 
			
		||||
 | 
			
		||||
import org.communiquons.android.comunic.client.BuildConfig;
 | 
			
		||||
import org.communiquons.android.comunic.client.R;
 | 
			
		||||
import org.communiquons.android.comunic.client.crashreporter.CrashReporter;
 | 
			
		||||
import org.communiquons.crashreporter.CrashReporter;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.enums.VirtualDirectoryType;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.helpers.APIRequestHelper;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.helpers.AccountHelper;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
package org.communiquons.android.comunic.client.ui.arrays;
 | 
			
		||||
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import org.communiquons.android.comunic.client.data.models.CallMember;
 | 
			
		||||
import org.communiquons.android.comunic.client.ui.models.CallPeerConnection;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * List of clients connections
 | 
			
		||||
 *
 | 
			
		||||
 * @author Pierre HUBERT
 | 
			
		||||
 */
 | 
			
		||||
public class CallPeersConnectionsList extends ArrayList<CallPeerConnection> {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find the connection matching a specific call member
 | 
			
		||||
     *
 | 
			
		||||
     *
 | 
			
		||||
     * @param member Information about the target member
 | 
			
		||||
     * @return Full client connection
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public CallPeerConnection find(CallMember member){
 | 
			
		||||
        for(CallPeerConnection connection : this)
 | 
			
		||||
            if(connection.getMember().getUserID() == member.getUserID())
 | 
			
		||||
                return connection;
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to find a peer connection using call ID
 | 
			
		||||
     *
 | 
			
		||||
     * @param id The ID of the user call ID
 | 
			
		||||
     * @return Information about the peer connection / null object in case of failure
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public CallPeerConnection findByCallID(String id){
 | 
			
		||||
        for(CallPeerConnection connection : this)
 | 
			
		||||
            if(connection.getMember().getUserCallID().equals(id))
 | 
			
		||||
                return connection;
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
package org.communiquons.android.comunic.client.ui.asynctasks;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
 | 
			
		||||
import org.communiquons.android.comunic.client.data.helpers.CallsHelper;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.models.CallInformation;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Task to get information about a call
 | 
			
		||||
 *
 | 
			
		||||
 * @author Pierre HUBERT
 | 
			
		||||
 */
 | 
			
		||||
public class GetCallInformationTask extends SafeAsyncTask<Integer, Void, CallInformation> {
 | 
			
		||||
 | 
			
		||||
    public GetCallInformationTask(Context context) {
 | 
			
		||||
        super(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected CallInformation doInBackground(Integer... integers) {
 | 
			
		||||
 | 
			
		||||
        CallsHelper callsHelper = new CallsHelper(getContext());
 | 
			
		||||
 | 
			
		||||
        CallInformation callInformation = callsHelper.getInfo(integers[0]);
 | 
			
		||||
 | 
			
		||||
        //Try to get call name
 | 
			
		||||
        if(callInformation == null || callsHelper.getCallName(callInformation) == null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        return callInformation;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,70 @@
 | 
			
		||||
package org.communiquons.android.comunic.client.ui.models;
 | 
			
		||||
 | 
			
		||||
import org.appspot.apprtc.PeerConnectionClient;
 | 
			
		||||
import org.communiquons.android.comunic.client.data.models.CallMember;
 | 
			
		||||
import org.webrtc.SurfaceViewRenderer;
 | 
			
		||||
import org.webrtc.VideoSink;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Single remote connection information
 | 
			
		||||
 *
 | 
			
		||||
 * @author Pierre HUBERT
 | 
			
		||||
 */
 | 
			
		||||
public class CallPeerConnection {
 | 
			
		||||
 | 
			
		||||
    //Private fields
 | 
			
		||||
    private CallMember member;
 | 
			
		||||
    private PeerConnectionClient peerConnectionClient;
 | 
			
		||||
    private ArrayList<VideoSink> remoteSinks = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
    //Views
 | 
			
		||||
    private SurfaceViewRenderer mLocalVideoView;
 | 
			
		||||
    private SurfaceViewRenderer mRemoteViewView;
 | 
			
		||||
 | 
			
		||||
    public CallPeerConnection(CallMember member) {
 | 
			
		||||
        this.member = member;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public CallMember getMember() {
 | 
			
		||||
        return member;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setMember(CallMember member) {
 | 
			
		||||
        this.member = member;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PeerConnectionClient getPeerConnectionClient() {
 | 
			
		||||
        return peerConnectionClient;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setPeerConnectionClient(PeerConnectionClient peerConnectionClient) {
 | 
			
		||||
        this.peerConnectionClient = peerConnectionClient;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ArrayList<VideoSink> getRemoteSinks() {
 | 
			
		||||
        return remoteSinks;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setRemoteSinks(ArrayList<VideoSink> remoteSinks) {
 | 
			
		||||
        this.remoteSinks = remoteSinks;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public SurfaceViewRenderer getRemoteViewView() {
 | 
			
		||||
        return mRemoteViewView;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setRemoteViewView(SurfaceViewRenderer mRemoteViewView) {
 | 
			
		||||
        this.mRemoteViewView = mRemoteViewView;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public SurfaceViewRenderer getLocalVideoView() {
 | 
			
		||||
 | 
			
		||||
        return mLocalVideoView;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setLocalVideoView(SurfaceViewRenderer mLocalVideoView) {
 | 
			
		||||
        this.mLocalVideoView = mLocalVideoView;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package org.communiquons.android.comunic.client.crashreporter;
 | 
			
		||||
package org.communiquons.crashreporter;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.os.AsyncTask;
 | 
			
		||||
@@ -0,0 +1,84 @@
 | 
			
		||||
package org.communiquons.signalexchangerclient;
 | 
			
		||||
 | 
			
		||||
import org.json.JSONException;
 | 
			
		||||
import org.json.JSONObject;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Signal exchanger client request
 | 
			
		||||
 *
 | 
			
		||||
 * @author Pierre HUBERT
 | 
			
		||||
 */
 | 
			
		||||
class ClientRequest {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Contains request information
 | 
			
		||||
     */
 | 
			
		||||
    private JSONObject mList;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize object
 | 
			
		||||
     */
 | 
			
		||||
    ClientRequest(){
 | 
			
		||||
        this.mList = new JSONObject();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a string to the request
 | 
			
		||||
     *
 | 
			
		||||
     * @param name The name of the string to add
 | 
			
		||||
     * @param value The value of the string to add
 | 
			
		||||
     * @return This object to help to concatenate requests
 | 
			
		||||
     */
 | 
			
		||||
    ClientRequest addString(String name, String value){
 | 
			
		||||
        try {
 | 
			
		||||
            mList.put(name, value);
 | 
			
		||||
        } catch (JSONException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
            throw new RuntimeException("Could not add a string to a JSON object!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a boolean to the request
 | 
			
		||||
     *
 | 
			
		||||
     * @param name The name of the string to add
 | 
			
		||||
     * @param value Boolean value
 | 
			
		||||
     * @return This object
 | 
			
		||||
     */
 | 
			
		||||
    ClientRequest addBoolean(String name, boolean value){
 | 
			
		||||
        try {
 | 
			
		||||
            mList.put(name, value);
 | 
			
		||||
        } catch (JSONException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a JSON object to the request
 | 
			
		||||
     *
 | 
			
		||||
     * @param name The name of the field to add
 | 
			
		||||
     * @param value The object
 | 
			
		||||
     * @return This object
 | 
			
		||||
     */
 | 
			
		||||
    ClientRequest addJSONObject(String name, JSONObject value){
 | 
			
		||||
        try {
 | 
			
		||||
            mList.put(name, value);
 | 
			
		||||
        } catch (JSONException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
        }
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get resulting JSON object
 | 
			
		||||
     *
 | 
			
		||||
     * @return Get the resulting JSON object
 | 
			
		||||
     */
 | 
			
		||||
    JSONObject get(){
 | 
			
		||||
        return mList;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,76 @@
 | 
			
		||||
package org.communiquons.signalexchangerclient;
 | 
			
		||||
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import org.webrtc.IceCandidate;
 | 
			
		||||
import org.webrtc.SessionDescription;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This interface should be implemented by the classes
 | 
			
		||||
 * that makes use of the {@link SignalExchangerClient}
 | 
			
		||||
 * in order to get updated about new information
 | 
			
		||||
 * availability
 | 
			
		||||
 *
 | 
			
		||||
 * @author Pierre HUBERT
 | 
			
		||||
 */
 | 
			
		||||
public interface SignalExchangerCallback {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called when an error occur
 | 
			
		||||
     *
 | 
			
		||||
     * @param msg Message associated to the error
 | 
			
		||||
     * @param t Optional associated throwable
 | 
			
		||||
     */
 | 
			
		||||
    void onSignalServerError(String msg, @Nullable Throwable t);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called once we are connected to the server
 | 
			
		||||
     */
 | 
			
		||||
    void onConnectedToSignalingServer();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called on ready message callback
 | 
			
		||||
     *
 | 
			
		||||
     * @param target_id The ID of the target
 | 
			
		||||
     * @param number_targets The number of peers who received the message
 | 
			
		||||
     */
 | 
			
		||||
    void onReadyMessageCallback(String target_id, int number_targets);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called when this client receive a new ready message signal
 | 
			
		||||
     *
 | 
			
		||||
     * @param source_id The source of the message
 | 
			
		||||
     */
 | 
			
		||||
    void onReadyMessage(String source_id);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method called when the client received a signal
 | 
			
		||||
     *
 | 
			
		||||
     * @param source_id The source of the signal
 | 
			
		||||
     * @param signal The signal
 | 
			
		||||
     */
 | 
			
		||||
    void onSignal(String source_id, String signal);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send signals callback
 | 
			
		||||
     *
 | 
			
		||||
     * @param number_targets The number of targets for the signal
 | 
			
		||||
     */
 | 
			
		||||
    void onSendSignalCallback(int number_targets);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This method is called once we received a remote Ice Candidate
 | 
			
		||||
     *
 | 
			
		||||
     * @param source_id The source of the signal
 | 
			
		||||
     * @param iceCandidate The candidate itself
 | 
			
		||||
     */
 | 
			
		||||
    void gotRemoteIceCandidate(String source_id, IceCandidate iceCandidate);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This method is called when we have got a new remote session description
 | 
			
		||||
     *
 | 
			
		||||
     * @param source_id The source of the signal
 | 
			
		||||
     * @param sessionDescription The session description
 | 
			
		||||
     */
 | 
			
		||||
    void gotRemoteSessionDescription(String source_id, SessionDescription sessionDescription);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,333 @@
 | 
			
		||||
package org.communiquons.signalexchangerclient;
 | 
			
		||||
 | 
			
		||||
import android.support.annotation.NonNull;
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import org.json.JSONException;
 | 
			
		||||
import org.json.JSONObject;
 | 
			
		||||
import org.webrtc.IceCandidate;
 | 
			
		||||
import org.webrtc.SessionDescription;
 | 
			
		||||
 | 
			
		||||
import okhttp3.OkHttpClient;
 | 
			
		||||
import okhttp3.Request;
 | 
			
		||||
import okhttp3.Response;
 | 
			
		||||
import okhttp3.WebSocket;
 | 
			
		||||
import okhttp3.WebSocketListener;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Signal exchanger client
 | 
			
		||||
 *
 | 
			
		||||
 * @author Pierre HUBERT
 | 
			
		||||
 */
 | 
			
		||||
public class SignalExchangerClient extends WebSocketListener {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Debug log
 | 
			
		||||
     */
 | 
			
		||||
    private static final String TAG = SignalExchangerClient.class.getSimpleName();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Instance configuration
 | 
			
		||||
     */
 | 
			
		||||
    private SignalExchangerInitConfig mConfig;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Signal exchanger callback
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private SignalExchangerCallback mCallback;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Http Client
 | 
			
		||||
     */
 | 
			
		||||
    private OkHttpClient mClient;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Current WebSocket connection
 | 
			
		||||
     */
 | 
			
		||||
    private WebSocket mWebSocket;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize a SignalExchanger client
 | 
			
		||||
     *
 | 
			
		||||
     * @param config Configuration of the client
 | 
			
		||||
     * @param cb Callback function to call when we got information update
 | 
			
		||||
     */
 | 
			
		||||
    public SignalExchangerClient(@NonNull SignalExchangerInitConfig config,
 | 
			
		||||
                                 @Nullable SignalExchangerCallback cb){
 | 
			
		||||
 | 
			
		||||
        //Save configuration
 | 
			
		||||
        this.mConfig = config;
 | 
			
		||||
        this.mCallback = cb;
 | 
			
		||||
 | 
			
		||||
        //Connect to the WebSocket
 | 
			
		||||
        String url = (config.isSecure() ? "wss" : "ws")
 | 
			
		||||
                + "://" + config.getDomain() + ":" + config.getPort() + "/socket";
 | 
			
		||||
 | 
			
		||||
        mClient = new OkHttpClient();
 | 
			
		||||
        Request request = new Request.Builder().url(url).build();
 | 
			
		||||
 | 
			
		||||
        mWebSocket = mClient.newWebSocket(request, this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get current client configuration
 | 
			
		||||
     *
 | 
			
		||||
     * @return Configuration of the client
 | 
			
		||||
     */
 | 
			
		||||
    public SignalExchangerInitConfig getConfig() {
 | 
			
		||||
        return mConfig;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the callback to use on new updates
 | 
			
		||||
     *
 | 
			
		||||
     * @param mCallback Callback to use
 | 
			
		||||
     */
 | 
			
		||||
    public void setCallback(@Nullable SignalExchangerCallback mCallback) {
 | 
			
		||||
        this.mCallback = mCallback;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check out whether the current client is connected to a server or not
 | 
			
		||||
     *
 | 
			
		||||
     * @return true if the client is connected to a server / false else
 | 
			
		||||
     */
 | 
			
		||||
    public boolean isConnected(){
 | 
			
		||||
        return mWebSocket != null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send ready message to a client
 | 
			
		||||
     *
 | 
			
		||||
     * @param target_client_id The ID of the target client
 | 
			
		||||
     */
 | 
			
		||||
    public void sendReadyMessage(String target_client_id){
 | 
			
		||||
        sendData(new ClientRequest()
 | 
			
		||||
                .addBoolean("ready_msg", true)
 | 
			
		||||
                .addString("target_id", target_client_id));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a signal to a target
 | 
			
		||||
     *
 | 
			
		||||
     * @param target_id The ID of the target
 | 
			
		||||
     * @param signal The signal to send
 | 
			
		||||
     */
 | 
			
		||||
    public void sendSignal(String target_id, String signal){
 | 
			
		||||
        sendData(new ClientRequest()
 | 
			
		||||
                .addString("target_id", target_id)
 | 
			
		||||
                .addString("signal", signal));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a session description to a target
 | 
			
		||||
     *
 | 
			
		||||
     * @param target_id The ID of the target
 | 
			
		||||
     * @param description The description
 | 
			
		||||
     */
 | 
			
		||||
    public void sendSessionDescription(String target_id, SessionDescription description){
 | 
			
		||||
        try {
 | 
			
		||||
            JSONObject object = new JSONObject();
 | 
			
		||||
            object.put("type", description.type.canonicalForm());
 | 
			
		||||
            object.put("sdp", description.description);
 | 
			
		||||
            sendSignal(target_id, object.toString());
 | 
			
		||||
 | 
			
		||||
        } catch (JSONException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send an Ice Candidate to a remote peer
 | 
			
		||||
     *
 | 
			
		||||
     * @param target_id The ID of the target
 | 
			
		||||
     * @param candidate The candidate to send
 | 
			
		||||
     */
 | 
			
		||||
    public void sendIceCandidate(String target_id, IceCandidate candidate){
 | 
			
		||||
        try {
 | 
			
		||||
            JSONObject candidateObj = new JSONObject();
 | 
			
		||||
            candidateObj.put("sdpMid", candidate.sdpMid);
 | 
			
		||||
            candidateObj.put("sdpMLineIndex", candidate.sdpMLineIndex);
 | 
			
		||||
            candidateObj.put("candidate", candidate.sdp);
 | 
			
		||||
 | 
			
		||||
            JSONObject object = new JSONObject();
 | 
			
		||||
            object.put("candidate", candidateObj);
 | 
			
		||||
            sendSignal(target_id, object.toString());
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        } catch (JSONException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send data to the server
 | 
			
		||||
     *
 | 
			
		||||
     * @param request The data to send to the server
 | 
			
		||||
     */
 | 
			
		||||
    private void sendData(@NonNull ClientRequest request){
 | 
			
		||||
 | 
			
		||||
        //Continues only in case of active connection
 | 
			
		||||
        if(!isConnected()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Send data to the server
 | 
			
		||||
        Log.v(TAG, "Sending " + request.get().toString());
 | 
			
		||||
        mWebSocket.send(request.get().toString());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invoked when a web socket has been accepted by the remote peer and may begin transmitting
 | 
			
		||||
     * messages.
 | 
			
		||||
     */
 | 
			
		||||
    public void onOpen(WebSocket webSocket, Response response) {
 | 
			
		||||
 | 
			
		||||
        //Save WebSocket object
 | 
			
		||||
        this.mWebSocket = webSocket;
 | 
			
		||||
 | 
			
		||||
        //Send the ID of current client to the server
 | 
			
		||||
        sendData(new ClientRequest()
 | 
			
		||||
                .addString("client_id", mConfig.getClientID()));
 | 
			
		||||
 | 
			
		||||
        //Inform we are connected
 | 
			
		||||
        if(mCallback != null)
 | 
			
		||||
            mCallback.onConnectedToSignalingServer();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Invoked when a text (type {@code 0x1}) message has been received. */
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onMessage(WebSocket webSocket, String text) {
 | 
			
		||||
        Log.v(TAG, "Received new message from server: " + text);
 | 
			
		||||
 | 
			
		||||
        //Decode message
 | 
			
		||||
        try {
 | 
			
		||||
            JSONObject message = new JSONObject(text);
 | 
			
		||||
 | 
			
		||||
            //Ready message callback
 | 
			
		||||
            if(message.has("ready_message_sent")){
 | 
			
		||||
 | 
			
		||||
                if(mCallback != null)
 | 
			
		||||
                    mCallback.onReadyMessageCallback(
 | 
			
		||||
                            message.getString("target_id"),
 | 
			
		||||
                            message.getInt("number_of_targets")
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Ready message
 | 
			
		||||
            else if(message.has("ready_msg")){
 | 
			
		||||
 | 
			
		||||
                if(mCallback != null)
 | 
			
		||||
                    mCallback.onReadyMessage(
 | 
			
		||||
                            message.getString("source_id")
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Signal
 | 
			
		||||
            else if(message.has("signal")) {
 | 
			
		||||
 | 
			
		||||
                if(mCallback != null)
 | 
			
		||||
                    mCallback.onSignal(
 | 
			
		||||
                            message.getString("source_id"),
 | 
			
		||||
                            message.getString("signal")
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                processReceivedSignal(message.getString("source_id"),
 | 
			
		||||
                        message.getString("signal"));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Send signal callback
 | 
			
		||||
            else if(message.has("signal_sent")){
 | 
			
		||||
 | 
			
		||||
                if(mCallback != null)
 | 
			
		||||
                    mCallback.onSendSignalCallback(
 | 
			
		||||
                            message.getInt("number_of_targets")
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Success message
 | 
			
		||||
            else if(message.has("success"))
 | 
			
		||||
                Log.v(TAG, "Success: " + message.getString("success"));
 | 
			
		||||
 | 
			
		||||
            //Unrecognized message
 | 
			
		||||
            else
 | 
			
		||||
                Log.e(TAG, "Message from server not understood!");
 | 
			
		||||
 | 
			
		||||
        } catch (JSONException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
 | 
			
		||||
            if(mCallback != null)
 | 
			
		||||
                mCallback.onSignalServerError("Could not parse response from server!", e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Process a received signal
 | 
			
		||||
     *
 | 
			
		||||
     * @param source_id The source of the signal
 | 
			
		||||
     * @param signal The signal to process
 | 
			
		||||
     */
 | 
			
		||||
    private void processReceivedSignal(String source_id, String signal) throws JSONException {
 | 
			
		||||
 | 
			
		||||
        JSONObject object = new JSONObject(signal);
 | 
			
		||||
 | 
			
		||||
        //Ice candidate
 | 
			
		||||
        if(object.has("candidate")) {
 | 
			
		||||
 | 
			
		||||
            JSONObject candidate = object.getJSONObject("candidate");
 | 
			
		||||
 | 
			
		||||
            if (mCallback != null)
 | 
			
		||||
                mCallback.gotRemoteIceCandidate(
 | 
			
		||||
                        source_id, new IceCandidate(
 | 
			
		||||
                                candidate.getString("sdpMid"),
 | 
			
		||||
                                candidate.getInt("sdpMLineIndex"),
 | 
			
		||||
                                candidate.getString("candidate")
 | 
			
		||||
                        )
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Sdp signal
 | 
			
		||||
        else if(object.has("sdp") && object.has("type")){
 | 
			
		||||
 | 
			
		||||
            SessionDescription.Type type = SessionDescription.Type.fromCanonicalForm(
 | 
			
		||||
                    object.getString("type"));
 | 
			
		||||
            String sdp = object.getString("sdp");
 | 
			
		||||
 | 
			
		||||
            if(mCallback != null)
 | 
			
		||||
                mCallback.gotRemoteSessionDescription(source_id,
 | 
			
		||||
                        new SessionDescription(type, sdp));
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        else
 | 
			
		||||
            Log.e(TAG, "Could not understand received signal!");
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invoked when both peers have indicated that no more messages will be transmitted and the
 | 
			
		||||
     * connection has been successfully released. No further calls to this listener will be made.
 | 
			
		||||
     */
 | 
			
		||||
    public void onClosed(WebSocket webSocket, int code, String reason) {
 | 
			
		||||
        mWebSocket = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invoked when a web socket has been closed due to an error reading from or writing to the
 | 
			
		||||
     * network. Both outgoing and incoming messages may have been lost. No further calls to this
 | 
			
		||||
     * listener will be made.
 | 
			
		||||
     */
 | 
			
		||||
    public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) {
 | 
			
		||||
 | 
			
		||||
        if(mCallback != null)
 | 
			
		||||
            mCallback.onSignalServerError(t.getMessage(), t);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
package org.communiquons.signalexchangerclient;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Signal exchanger configuration intialization
 | 
			
		||||
 *
 | 
			
		||||
 * @author Pierre HUBERT
 | 
			
		||||
 */
 | 
			
		||||
public class SignalExchangerInitConfig {
 | 
			
		||||
 | 
			
		||||
    //Private fields
 | 
			
		||||
    private String domain;
 | 
			
		||||
    private int port;
 | 
			
		||||
    private String clientID;
 | 
			
		||||
    private boolean isSecure;
 | 
			
		||||
 | 
			
		||||
    public SignalExchangerInitConfig() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public SignalExchangerInitConfig(String domain, int port, String clientID, boolean isSecure) {
 | 
			
		||||
        this.domain = domain;
 | 
			
		||||
        this.port = port;
 | 
			
		||||
        this.clientID = clientID;
 | 
			
		||||
        this.isSecure = isSecure;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String getDomain() {
 | 
			
		||||
        return domain;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setDomain(String domain) {
 | 
			
		||||
        this.domain = domain;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int getPort() {
 | 
			
		||||
        return port;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setPort(int port) {
 | 
			
		||||
        this.port = port;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String getClientID() {
 | 
			
		||||
        return clientID;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setClientID(String clientID) {
 | 
			
		||||
        this.clientID = clientID;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isSecure() {
 | 
			
		||||
        return isSecure;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setSecure(boolean secure) {
 | 
			
		||||
        isSecure = secure;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,17 +6,18 @@
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    tools:context=".ui.activities.CallActivity">
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
        android:id="@+id/call_id"
 | 
			
		||||
    <ProgressBar
 | 
			
		||||
        android:id="@+id/progressBar"
 | 
			
		||||
        style="?android:attr/progressBarStyle"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="21dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginStart="8dp"
 | 
			
		||||
        android:layout_marginTop="8dp"
 | 
			
		||||
        android:layout_marginEnd="8dp"
 | 
			
		||||
        android:layout_marginBottom="8dp"
 | 
			
		||||
        android:text="TextView"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="parent"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent" />
 | 
			
		||||
 | 
			
		||||
</android.support.constraint.ConstraintLayout>
 | 
			
		||||
@@ -329,4 +329,7 @@
 | 
			
		||||
    <string name="button_accept_call">Répondre</string>
 | 
			
		||||
    <string name="title_incoming_call">Appel entrant</string>
 | 
			
		||||
    <string name="err_get_next_pending_call_info">Impossible de récupérer les informations de l\'appel en cours !</string>
 | 
			
		||||
    <string name="err_get_call_info">Impossible de récupérer les infoamtions sur l\'appel !</string>
 | 
			
		||||
    <string name="err_connect_signaling_server">Impossible de connecter au signaling server !</string>
 | 
			
		||||
    <string name="notice_call_terminated">Appel terminé.</string>
 | 
			
		||||
</resources>
 | 
			
		||||
@@ -328,4 +328,7 @@
 | 
			
		||||
    <string name="button_accept_call">Accept call</string>
 | 
			
		||||
    <string name="title_incoming_call">Incoming call</string>
 | 
			
		||||
    <string name="err_get_next_pending_call_info">Could not get pending call information!</string>
 | 
			
		||||
    <string name="err_get_call_info">Could not get call information!</string>
 | 
			
		||||
    <string name="err_connect_signaling_server">Could not connect to signaling server!</string>
 | 
			
		||||
    <string name="notice_call_terminated">Call terminated</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user