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
onCreate(Bundle savedInstanceState)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
onPause()271   public void onPause() {
272     super.onPause();
273     activityRunning = false;
274     if (peerConnectionClient != null) {
275       peerConnectionClient.stopVideoSource();
276     }
277   }
278 
279   @Override
onResume()280   public void onResume() {
281     super.onResume();
282     activityRunning = true;
283     if (peerConnectionClient != null) {
284       peerConnectionClient.startVideoSource();
285     }
286   }
287 
288   @Override
onDestroy()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
onCallHangUp()301   public void onCallHangUp() {
302     disconnect();
303   }
304 
305   @Override
onCameraSwitch()306   public void onCameraSwitch() {
307     if (peerConnectionClient != null) {
308       peerConnectionClient.switchCamera();
309     }
310   }
311 
312   @Override
onVideoScalingSwitch(ScalingType scalingType)313   public void onVideoScalingSwitch(ScalingType scalingType) {
314     this.scalingType = scalingType;
315     updateVideoView();
316   }
317 
318   @Override
onCaptureFormatChange(int width, int height, int framerate)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.
toggleCallControlFragmentVisibility()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 
updateVideoView()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 
startCall()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
callConnected()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 
onAudioManagerChangedState()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.
disconnect()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 
disconnectWithErrorMessage(final String errorMessage)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.
logAndToast(String msg)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 
reportError(final String description)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.
onConnectedToRoomInternal(final SignalingParameters params)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
onConnectedToRoom(final SignalingParameters params)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
onRemoteDescription(final SessionDescription sdp)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
onRemoteIceCandidate(final IceCandidate candidate)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
onChannelClose()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
onChannelError(final String description)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
onLocalDescription(final SessionDescription sdp)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
onIceCandidate(final IceCandidate candidate)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
onIceConnected()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
onIceDisconnected()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
onPeerConnectionClosed()640   public void onPeerConnectionClosed() {
641   }
642 
643   @Override
onPeerConnectionStatsReady(final StatsReport[] reports)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
onPeerConnectionError(final String description)656   public void onPeerConnectionError(final String description) {
657     reportError(description);
658   }
659 }
660