Home | History | Annotate | Download | only in apprtc
      1 /*
      2  *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
      3  *
      4  *  Use of this source code is governed by a BSD-style license
      5  *  that can be found in the LICENSE file in the root of the source
      6  *  tree. An additional intellectual property rights grant can be found
      7  *  in the file PATENTS.  All contributing project authors may
      8  *  be found in the AUTHORS file in the root of the source tree.
      9  */
     10 
     11 package org.appspot.apprtc;
     12 
     13 import org.appspot.apprtc.AppRTCClient.RoomConnectionParameters;
     14 import org.appspot.apprtc.AppRTCClient.SignalingParameters;
     15 import org.appspot.apprtc.PeerConnectionClient.PeerConnectionParameters;
     16 import org.appspot.apprtc.util.LooperExecutor;
     17 
     18 import android.app.Activity;
     19 import android.app.AlertDialog;
     20 import android.app.FragmentTransaction;
     21 import android.content.DialogInterface;
     22 import android.content.Intent;
     23 import android.content.pm.PackageManager;
     24 import android.net.Uri;
     25 import android.os.Bundle;
     26 import android.os.Handler;
     27 import android.util.Log;
     28 import android.view.View;
     29 import android.view.Window;
     30 import android.view.WindowManager.LayoutParams;
     31 import android.widget.Toast;
     32 
     33 import org.webrtc.EglBase;
     34 import org.webrtc.IceCandidate;
     35 import org.webrtc.SessionDescription;
     36 import org.webrtc.StatsReport;
     37 import org.webrtc.RendererCommon.ScalingType;
     38 import org.webrtc.SurfaceViewRenderer;
     39 
     40 /**
     41  * Activity for peer connection call setup, call waiting
     42  * and call view.
     43  */
     44 public class CallActivity extends Activity
     45     implements AppRTCClient.SignalingEvents,
     46       PeerConnectionClient.PeerConnectionEvents,
     47       CallFragment.OnCallEvents {
     48 
     49   public static final String EXTRA_ROOMID =
     50       "org.appspot.apprtc.ROOMID";
     51   public static final String EXTRA_LOOPBACK =
     52       "org.appspot.apprtc.LOOPBACK";
     53   public static final String EXTRA_VIDEO_CALL =
     54       "org.appspot.apprtc.VIDEO_CALL";
     55   public static final String EXTRA_VIDEO_WIDTH =
     56       "org.appspot.apprtc.VIDEO_WIDTH";
     57   public static final String EXTRA_VIDEO_HEIGHT =
     58       "org.appspot.apprtc.VIDEO_HEIGHT";
     59   public static final String EXTRA_VIDEO_FPS =
     60       "org.appspot.apprtc.VIDEO_FPS";
     61   public static final String EXTRA_VIDEO_CAPTUREQUALITYSLIDER_ENABLED =
     62       "org.appsopt.apprtc.VIDEO_CAPTUREQUALITYSLIDER";
     63   public static final String EXTRA_VIDEO_BITRATE =
     64       "org.appspot.apprtc.VIDEO_BITRATE";
     65   public static final String EXTRA_VIDEOCODEC =
     66       "org.appspot.apprtc.VIDEOCODEC";
     67   public static final String EXTRA_HWCODEC_ENABLED =
     68       "org.appspot.apprtc.HWCODEC";
     69   public static final String EXTRA_CAPTURETOTEXTURE_ENABLED =
     70       "org.appspot.apprtc.CAPTURETOTEXTURE";
     71   public static final String EXTRA_AUDIO_BITRATE =
     72       "org.appspot.apprtc.AUDIO_BITRATE";
     73   public static final String EXTRA_AUDIOCODEC =
     74       "org.appspot.apprtc.AUDIOCODEC";
     75   public static final String EXTRA_NOAUDIOPROCESSING_ENABLED =
     76       "org.appspot.apprtc.NOAUDIOPROCESSING";
     77   public static final String EXTRA_AECDUMP_ENABLED =
     78       "org.appspot.apprtc.AECDUMP";
     79   public static final String EXTRA_OPENSLES_ENABLED =
     80       "org.appspot.apprtc.OPENSLES";
     81   public static final String EXTRA_DISPLAY_HUD =
     82       "org.appspot.apprtc.DISPLAY_HUD";
     83   public static final String EXTRA_TRACING = "org.appspot.apprtc.TRACING";
     84   public static final String EXTRA_CMDLINE =
     85       "org.appspot.apprtc.CMDLINE";
     86   public static final String EXTRA_RUNTIME =
     87       "org.appspot.apprtc.RUNTIME";
     88   private static final String TAG = "CallRTCClient";
     89 
     90   // List of mandatory application permissions.
     91   private static final String[] MANDATORY_PERMISSIONS = {
     92     "android.permission.MODIFY_AUDIO_SETTINGS",
     93     "android.permission.RECORD_AUDIO",
     94     "android.permission.INTERNET"
     95   };
     96 
     97   // Peer connection statistics callback period in ms.
     98   private static final int STAT_CALLBACK_PERIOD = 1000;
     99   // Local preview screen position before call is connected.
    100   private static final int LOCAL_X_CONNECTING = 0;
    101   private static final int LOCAL_Y_CONNECTING = 0;
    102   private static final int LOCAL_WIDTH_CONNECTING = 100;
    103   private static final int LOCAL_HEIGHT_CONNECTING = 100;
    104   // Local preview screen position after call is connected.
    105   private static final int LOCAL_X_CONNECTED = 72;
    106   private static final int LOCAL_Y_CONNECTED = 72;
    107   private static final int LOCAL_WIDTH_CONNECTED = 25;
    108   private static final int LOCAL_HEIGHT_CONNECTED = 25;
    109   // Remote video screen position
    110   private static final int REMOTE_X = 0;
    111   private static final int REMOTE_Y = 0;
    112   private static final int REMOTE_WIDTH = 100;
    113   private static final int REMOTE_HEIGHT = 100;
    114   private PeerConnectionClient peerConnectionClient = null;
    115   private AppRTCClient appRtcClient;
    116   private SignalingParameters signalingParameters;
    117   private AppRTCAudioManager audioManager = null;
    118   private EglBase rootEglBase;
    119   private SurfaceViewRenderer localRender;
    120   private SurfaceViewRenderer remoteRender;
    121   private PercentFrameLayout localRenderLayout;
    122   private PercentFrameLayout remoteRenderLayout;
    123   private ScalingType scalingType;
    124   private Toast logToast;
    125   private boolean commandLineRun;
    126   private int runTimeMs;
    127   private boolean activityRunning;
    128   private RoomConnectionParameters roomConnectionParameters;
    129   private PeerConnectionParameters peerConnectionParameters;
    130   private boolean iceConnected;
    131   private boolean isError;
    132   private boolean callControlFragmentVisible = true;
    133   private long callStartedTimeMs = 0;
    134 
    135   // Controls
    136   CallFragment callFragment;
    137   HudFragment hudFragment;
    138 
    139   @Override
    140   public void onCreate(Bundle savedInstanceState) {
    141     super.onCreate(savedInstanceState);
    142     Thread.setDefaultUncaughtExceptionHandler(
    143         new UnhandledExceptionHandler(this));
    144 
    145     // Set window styles for fullscreen-window size. Needs to be done before
    146     // adding content.
    147     requestWindowFeature(Window.FEATURE_NO_TITLE);
    148     getWindow().addFlags(
    149         LayoutParams.FLAG_FULLSCREEN
    150         | LayoutParams.FLAG_KEEP_SCREEN_ON
    151         | LayoutParams.FLAG_DISMISS_KEYGUARD
    152         | LayoutParams.FLAG_SHOW_WHEN_LOCKED
    153         | LayoutParams.FLAG_TURN_SCREEN_ON);
    154     getWindow().getDecorView().setSystemUiVisibility(
    155         View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
    156         | View.SYSTEM_UI_FLAG_FULLSCREEN
    157         | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
    158     setContentView(R.layout.activity_call);
    159 
    160     iceConnected = false;
    161     signalingParameters = null;
    162     scalingType = ScalingType.SCALE_ASPECT_FILL;
    163 
    164     // Create UI controls.
    165     localRender = (SurfaceViewRenderer) findViewById(R.id.local_video_view);
    166     remoteRender = (SurfaceViewRenderer) findViewById(R.id.remote_video_view);
    167     localRenderLayout = (PercentFrameLayout) findViewById(R.id.local_video_layout);
    168     remoteRenderLayout = (PercentFrameLayout) findViewById(R.id.remote_video_layout);
    169     callFragment = new CallFragment();
    170     hudFragment = new HudFragment();
    171 
    172     // Show/hide call control fragment on view click.
    173     View.OnClickListener listener = new View.OnClickListener() {
    174       @Override
    175       public void onClick(View view) {
    176         toggleCallControlFragmentVisibility();
    177       }
    178     };
    179 
    180     localRender.setOnClickListener(listener);
    181     remoteRender.setOnClickListener(listener);
    182 
    183     // Create video renderers.
    184     rootEglBase = EglBase.create();
    185     localRender.init(rootEglBase.getEglBaseContext(), null);
    186     remoteRender.init(rootEglBase.getEglBaseContext(), null);
    187     localRender.setZOrderMediaOverlay(true);
    188     updateVideoView();
    189 
    190     // Check for mandatory permissions.
    191     for (String permission : MANDATORY_PERMISSIONS) {
    192       if (checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
    193         logAndToast("Permission " + permission + " is not granted");
    194         setResult(RESULT_CANCELED);
    195         finish();
    196         return;
    197       }
    198     }
    199 
    200     // Get Intent parameters.
    201     final Intent intent = getIntent();
    202     Uri roomUri = intent.getData();
    203     if (roomUri == null) {
    204       logAndToast(getString(R.string.missing_url));
    205       Log.e(TAG, "Didn't get any URL in intent!");
    206       setResult(RESULT_CANCELED);
    207       finish();
    208       return;
    209     }
    210     String roomId = intent.getStringExtra(EXTRA_ROOMID);
    211     if (roomId == null || roomId.length() == 0) {
    212       logAndToast(getString(R.string.missing_url));
    213       Log.e(TAG, "Incorrect room ID in intent!");
    214       setResult(RESULT_CANCELED);
    215       finish();
    216       return;
    217     }
    218     boolean loopback = intent.getBooleanExtra(EXTRA_LOOPBACK, false);
    219     boolean tracing = intent.getBooleanExtra(EXTRA_TRACING, false);
    220     peerConnectionParameters = new PeerConnectionParameters(
    221         intent.getBooleanExtra(EXTRA_VIDEO_CALL, true),
    222         loopback,
    223         tracing,
    224         intent.getIntExtra(EXTRA_VIDEO_WIDTH, 0),
    225         intent.getIntExtra(EXTRA_VIDEO_HEIGHT, 0),
    226         intent.getIntExtra(EXTRA_VIDEO_FPS, 0),
    227         intent.getIntExtra(EXTRA_VIDEO_BITRATE, 0),
    228         intent.getStringExtra(EXTRA_VIDEOCODEC),
    229         intent.getBooleanExtra(EXTRA_HWCODEC_ENABLED, true),
    230         intent.getBooleanExtra(EXTRA_CAPTURETOTEXTURE_ENABLED, false),
    231         intent.getIntExtra(EXTRA_AUDIO_BITRATE, 0),
    232         intent.getStringExtra(EXTRA_AUDIOCODEC),
    233         intent.getBooleanExtra(EXTRA_NOAUDIOPROCESSING_ENABLED, false),
    234         intent.getBooleanExtra(EXTRA_AECDUMP_ENABLED, false),
    235         intent.getBooleanExtra(EXTRA_OPENSLES_ENABLED, false));
    236     commandLineRun = intent.getBooleanExtra(EXTRA_CMDLINE, false);
    237     runTimeMs = intent.getIntExtra(EXTRA_RUNTIME, 0);
    238 
    239     // Create connection client and connection parameters.
    240     appRtcClient = new WebSocketRTCClient(this, new LooperExecutor());
    241     roomConnectionParameters = new RoomConnectionParameters(
    242         roomUri.toString(), roomId, loopback);
    243 
    244     // Send intent arguments to fragments.
    245     callFragment.setArguments(intent.getExtras());
    246     hudFragment.setArguments(intent.getExtras());
    247     // Activate call and HUD fragments and start the call.
    248     FragmentTransaction ft = getFragmentManager().beginTransaction();
    249     ft.add(R.id.call_fragment_container, callFragment);
    250     ft.add(R.id.hud_fragment_container, hudFragment);
    251     ft.commit();
    252     startCall();
    253 
    254     // For command line execution run connection for <runTimeMs> and exit.
    255     if (commandLineRun && runTimeMs > 0) {
    256       (new Handler()).postDelayed(new Runnable() {
    257         @Override
    258         public void run() {
    259           disconnect();
    260         }
    261       }, runTimeMs);
    262     }
    263 
    264     peerConnectionClient = PeerConnectionClient.getInstance();
    265     peerConnectionClient.createPeerConnectionFactory(
    266         CallActivity.this, peerConnectionParameters, CallActivity.this);
    267   }
    268 
    269   // Activity interfaces
    270   @Override
    271   public void onPause() {
    272     super.onPause();
    273     activityRunning = false;
    274     if (peerConnectionClient != null) {
    275       peerConnectionClient.stopVideoSource();
    276     }
    277   }
    278 
    279   @Override
    280   public void onResume() {
    281     super.onResume();
    282     activityRunning = true;
    283     if (peerConnectionClient != null) {
    284       peerConnectionClient.startVideoSource();
    285     }
    286   }
    287 
    288   @Override
    289   protected void onDestroy() {
    290     disconnect();
    291     if (logToast != null) {
    292       logToast.cancel();
    293     }
    294     activityRunning = false;
    295     rootEglBase.release();
    296     super.onDestroy();
    297   }
    298 
    299   // CallFragment.OnCallEvents interface implementation.
    300   @Override
    301   public void onCallHangUp() {
    302     disconnect();
    303   }
    304 
    305   @Override
    306   public void onCameraSwitch() {
    307     if (peerConnectionClient != null) {
    308       peerConnectionClient.switchCamera();
    309     }
    310   }
    311 
    312   @Override
    313   public void onVideoScalingSwitch(ScalingType scalingType) {
    314     this.scalingType = scalingType;
    315     updateVideoView();
    316   }
    317 
    318   @Override
    319   public void onCaptureFormatChange(int width, int height, int framerate) {
    320     if (peerConnectionClient != null) {
    321       peerConnectionClient.changeCaptureFormat(width, height, framerate);
    322     }
    323   }
    324 
    325   // Helper functions.
    326   private void toggleCallControlFragmentVisibility() {
    327     if (!iceConnected || !callFragment.isAdded()) {
    328       return;
    329     }
    330     // Show/hide call control fragment
    331     callControlFragmentVisible = !callControlFragmentVisible;
    332     FragmentTransaction ft = getFragmentManager().beginTransaction();
    333     if (callControlFragmentVisible) {
    334       ft.show(callFragment);
    335       ft.show(hudFragment);
    336     } else {
    337       ft.hide(callFragment);
    338       ft.hide(hudFragment);
    339     }
    340     ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
    341     ft.commit();
    342   }
    343 
    344   private void updateVideoView() {
    345     remoteRenderLayout.setPosition(REMOTE_X, REMOTE_Y, REMOTE_WIDTH, REMOTE_HEIGHT);
    346     remoteRender.setScalingType(scalingType);
    347     remoteRender.setMirror(false);
    348 
    349     if (iceConnected) {
    350       localRenderLayout.setPosition(
    351           LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED, LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED);
    352       localRender.setScalingType(ScalingType.SCALE_ASPECT_FIT);
    353     } else {
    354       localRenderLayout.setPosition(
    355           LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING, LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING);
    356       localRender.setScalingType(scalingType);
    357     }
    358     localRender.setMirror(true);
    359 
    360     localRender.requestLayout();
    361     remoteRender.requestLayout();
    362   }
    363 
    364   private void startCall() {
    365     if (appRtcClient == null) {
    366       Log.e(TAG, "AppRTC client is not allocated for a call.");
    367       return;
    368     }
    369     callStartedTimeMs = System.currentTimeMillis();
    370 
    371     // Start room connection.
    372     logAndToast(getString(R.string.connecting_to,
    373         roomConnectionParameters.roomUrl));
    374     appRtcClient.connectToRoom(roomConnectionParameters);
    375 
    376     // Create and audio manager that will take care of audio routing,
    377     // audio modes, audio device enumeration etc.
    378     audioManager = AppRTCAudioManager.create(this, new Runnable() {
    379         // This method will be called each time the audio state (number and
    380         // type of devices) has been changed.
    381         @Override
    382         public void run() {
    383           onAudioManagerChangedState();
    384         }
    385       }
    386     );
    387     // Store existing audio settings and change audio mode to
    388     // MODE_IN_COMMUNICATION for best possible VoIP performance.
    389     Log.d(TAG, "Initializing the audio manager...");
    390     audioManager.init();
    391   }
    392 
    393   // Should be called from UI thread
    394   private void callConnected() {
    395     final long delta = System.currentTimeMillis() - callStartedTimeMs;
    396     Log.i(TAG, "Call connected: delay=" + delta + "ms");
    397     if (peerConnectionClient == null || isError) {
    398       Log.w(TAG, "Call is connected in closed or error state");
    399       return;
    400     }
    401     // Update video view.
    402     updateVideoView();
    403     // Enable statistics callback.
    404     peerConnectionClient.enableStatsEvents(true, STAT_CALLBACK_PERIOD);
    405   }
    406 
    407   private void onAudioManagerChangedState() {
    408     // TODO(henrika): disable video if AppRTCAudioManager.AudioDevice.EARPIECE
    409     // is active.
    410   }
    411 
    412   // Disconnect from remote resources, dispose of local resources, and exit.
    413   private void disconnect() {
    414     activityRunning = false;
    415     if (appRtcClient != null) {
    416       appRtcClient.disconnectFromRoom();
    417       appRtcClient = null;
    418     }
    419     if (peerConnectionClient != null) {
    420       peerConnectionClient.close();
    421       peerConnectionClient = null;
    422     }
    423     if (localRender != null) {
    424       localRender.release();
    425       localRender = null;
    426     }
    427     if (remoteRender != null) {
    428       remoteRender.release();
    429       remoteRender = null;
    430     }
    431     if (audioManager != null) {
    432       audioManager.close();
    433       audioManager = null;
    434     }
    435     if (iceConnected && !isError) {
    436       setResult(RESULT_OK);
    437     } else {
    438       setResult(RESULT_CANCELED);
    439     }
    440     finish();
    441   }
    442 
    443   private void disconnectWithErrorMessage(final String errorMessage) {
    444     if (commandLineRun || !activityRunning) {
    445       Log.e(TAG, "Critical error: " + errorMessage);
    446       disconnect();
    447     } else {
    448       new AlertDialog.Builder(this)
    449           .setTitle(getText(R.string.channel_error_title))
    450           .setMessage(errorMessage)
    451           .setCancelable(false)
    452           .setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() {
    453             @Override
    454             public void onClick(DialogInterface dialog, int id) {
    455               dialog.cancel();
    456               disconnect();
    457             }
    458           }).create().show();
    459     }
    460   }
    461 
    462   // Log |msg| and Toast about it.
    463   private void logAndToast(String msg) {
    464     Log.d(TAG, msg);
    465     if (logToast != null) {
    466       logToast.cancel();
    467     }
    468     logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
    469     logToast.show();
    470   }
    471 
    472   private void reportError(final String description) {
    473     runOnUiThread(new Runnable() {
    474       @Override
    475       public void run() {
    476         if (!isError) {
    477           isError = true;
    478           disconnectWithErrorMessage(description);
    479         }
    480       }
    481     });
    482   }
    483 
    484   // -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
    485   // All callbacks are invoked from websocket signaling looper thread and
    486   // are routed to UI thread.
    487   private void onConnectedToRoomInternal(final SignalingParameters params) {
    488     final long delta = System.currentTimeMillis() - callStartedTimeMs;
    489 
    490     signalingParameters = params;
    491     logAndToast("Creating peer connection, delay=" + delta + "ms");
    492     peerConnectionClient.createPeerConnection(rootEglBase.getEglBaseContext(),
    493         localRender, remoteRender, signalingParameters);
    494 
    495     if (signalingParameters.initiator) {
    496       logAndToast("Creating OFFER...");
    497       // Create offer. Offer SDP will be sent to answering client in
    498       // PeerConnectionEvents.onLocalDescription event.
    499       peerConnectionClient.createOffer();
    500     } else {
    501       if (params.offerSdp != null) {
    502         peerConnectionClient.setRemoteDescription(params.offerSdp);
    503         logAndToast("Creating ANSWER...");
    504         // Create answer. Answer SDP will be sent to offering client in
    505         // PeerConnectionEvents.onLocalDescription event.
    506         peerConnectionClient.createAnswer();
    507       }
    508       if (params.iceCandidates != null) {
    509         // Add remote ICE candidates from room.
    510         for (IceCandidate iceCandidate : params.iceCandidates) {
    511           peerConnectionClient.addRemoteIceCandidate(iceCandidate);
    512         }
    513       }
    514     }
    515   }
    516 
    517   @Override
    518   public void onConnectedToRoom(final SignalingParameters params) {
    519     runOnUiThread(new Runnable() {
    520       @Override
    521       public void run() {
    522         onConnectedToRoomInternal(params);
    523       }
    524     });
    525   }
    526 
    527   @Override
    528   public void onRemoteDescription(final SessionDescription sdp) {
    529     final long delta = System.currentTimeMillis() - callStartedTimeMs;
    530     runOnUiThread(new Runnable() {
    531       @Override
    532       public void run() {
    533         if (peerConnectionClient == null) {
    534           Log.e(TAG, "Received remote SDP for non-initilized peer connection.");
    535           return;
    536         }
    537         logAndToast("Received remote " + sdp.type + ", delay=" + delta + "ms");
    538         peerConnectionClient.setRemoteDescription(sdp);
    539         if (!signalingParameters.initiator) {
    540           logAndToast("Creating ANSWER...");
    541           // Create answer. Answer SDP will be sent to offering client in
    542           // PeerConnectionEvents.onLocalDescription event.
    543           peerConnectionClient.createAnswer();
    544         }
    545       }
    546     });
    547   }
    548 
    549   @Override
    550   public void onRemoteIceCandidate(final IceCandidate candidate) {
    551     runOnUiThread(new Runnable() {
    552       @Override
    553       public void run() {
    554         if (peerConnectionClient == null) {
    555           Log.e(TAG,
    556               "Received ICE candidate for non-initilized peer connection.");
    557           return;
    558         }
    559         peerConnectionClient.addRemoteIceCandidate(candidate);
    560       }
    561     });
    562   }
    563 
    564   @Override
    565   public void onChannelClose() {
    566     runOnUiThread(new Runnable() {
    567       @Override
    568       public void run() {
    569         logAndToast("Remote end hung up; dropping PeerConnection");
    570         disconnect();
    571       }
    572     });
    573   }
    574 
    575   @Override
    576   public void onChannelError(final String description) {
    577     reportError(description);
    578   }
    579 
    580   // -----Implementation of PeerConnectionClient.PeerConnectionEvents.---------
    581   // Send local peer connection SDP and ICE candidates to remote party.
    582   // All callbacks are invoked from peer connection client looper thread and
    583   // are routed to UI thread.
    584   @Override
    585   public void onLocalDescription(final SessionDescription sdp) {
    586     final long delta = System.currentTimeMillis() - callStartedTimeMs;
    587     runOnUiThread(new Runnable() {
    588       @Override
    589       public void run() {
    590         if (appRtcClient != null) {
    591           logAndToast("Sending " + sdp.type + ", delay=" + delta + "ms");
    592           if (signalingParameters.initiator) {
    593             appRtcClient.sendOfferSdp(sdp);
    594           } else {
    595             appRtcClient.sendAnswerSdp(sdp);
    596           }
    597         }
    598       }
    599     });
    600   }
    601 
    602   @Override
    603   public void onIceCandidate(final IceCandidate candidate) {
    604     runOnUiThread(new Runnable() {
    605       @Override
    606       public void run() {
    607         if (appRtcClient != null) {
    608           appRtcClient.sendLocalIceCandidate(candidate);
    609         }
    610       }
    611     });
    612   }
    613 
    614   @Override
    615   public void onIceConnected() {
    616     final long delta = System.currentTimeMillis() - callStartedTimeMs;
    617     runOnUiThread(new Runnable() {
    618       @Override
    619       public void run() {
    620         logAndToast("ICE connected, delay=" + delta + "ms");
    621         iceConnected = true;
    622         callConnected();
    623       }
    624     });
    625   }
    626 
    627   @Override
    628   public void onIceDisconnected() {
    629     runOnUiThread(new Runnable() {
    630       @Override
    631       public void run() {
    632         logAndToast("ICE disconnected");
    633         iceConnected = false;
    634         disconnect();
    635       }
    636     });
    637   }
    638 
    639   @Override
    640   public void onPeerConnectionClosed() {
    641   }
    642 
    643   @Override
    644   public void onPeerConnectionStatsReady(final StatsReport[] reports) {
    645     runOnUiThread(new Runnable() {
    646       @Override
    647       public void run() {
    648         if (!isError && iceConnected) {
    649           hudFragment.updateEncoderStatistics(reports);
    650         }
    651       }
    652     });
    653   }
    654 
    655   @Override
    656   public void onPeerConnectionError(final String description) {
    657     reportError(description);
    658   }
    659 }
    660