Port some fixes in AppRTCDemo.

- Make PeerConnectionClient a singleton.
- Fix crash in CpuMonitor.
- Remove reading constraints from room response.
- Catch and report camera errors.

R=wzh@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/43059004

Cr-Commit-Position: refs/heads/master@{#8930}
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java
index 20f8a4a..30451bf 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java
@@ -28,7 +28,6 @@
 package org.appspot.apprtc;
 
 import org.webrtc.IceCandidate;
-import org.webrtc.MediaConstraints;
 import org.webrtc.PeerConnection;
 import org.webrtc.SessionDescription;
 
@@ -87,9 +86,6 @@
   public static class SignalingParameters {
     public final List<PeerConnection.IceServer> iceServers;
     public final boolean initiator;
-    public final MediaConstraints pcConstraints;
-    public final MediaConstraints videoConstraints;
-    public final MediaConstraints audioConstraints;
     public final String clientId;
     public final String wssUrl;
     public final String wssPostUrl;
@@ -98,15 +94,11 @@
 
     public SignalingParameters(
         List<PeerConnection.IceServer> iceServers,
-        boolean initiator, MediaConstraints pcConstraints,
-        MediaConstraints videoConstraints, MediaConstraints audioConstraints,
-        String clientId, String wssUrl, String wssPostUrl,
+        boolean initiator, String clientId,
+        String wssUrl, String wssPostUrl,
         SessionDescription offerSdp, List<IceCandidate> iceCandidates) {
       this.iceServers = iceServers;
       this.initiator = initiator;
-      this.pcConstraints = pcConstraints;
-      this.videoConstraints = videoConstraints;
-      this.audioConstraints = audioConstraints;
       this.clientId = clientId;
       this.wssUrl = wssUrl;
       this.wssPostUrl = wssPostUrl;
diff --git a/talk/examples/android/src/org/appspot/apprtc/CallActivity.java b/talk/examples/android/src/org/appspot/apprtc/CallActivity.java
index a4255b5..e4382d1 100644
--- a/talk/examples/android/src/org/appspot/apprtc/CallActivity.java
+++ b/talk/examples/android/src/org/appspot/apprtc/CallActivity.java
@@ -383,7 +383,7 @@
         if (peerConnectionClient == null) {
           final long delta = System.currentTimeMillis() - callStartedTimeMs;
           Log.d(TAG, "Creating peer connection factory, delay=" + delta + "ms");
-          peerConnectionClient = new PeerConnectionClient();
+          peerConnectionClient = PeerConnectionClient.getInstance();
           peerConnectionClient.createPeerConnectionFactory(CallActivity.this,
               VideoRendererGui.getEGLContext(), peerConnectionParameters,
               CallActivity.this);
diff --git a/talk/examples/android/src/org/appspot/apprtc/CpuMonitor.java b/talk/examples/android/src/org/appspot/apprtc/CpuMonitor.java
index 327d47e..89d21d4 100644
--- a/talk/examples/android/src/org/appspot/apprtc/CpuMonitor.java
+++ b/talk/examples/android/src/org/appspot/apprtc/CpuMonitor.java
@@ -113,7 +113,8 @@
         Scanner scanner = new Scanner(rdr).useDelimiter("[-\n]");
         scanner.nextInt();  // Skip leading number 0.
         cpusPresent = 1 + scanner.nextInt();
-      } catch (InputMismatchException e) {
+        scanner.close();
+      } catch (Exception e) {
         Log.e(TAG, "Cannot do CPU stats due to /sys/devices/system/cpu/present parsing problem");
       } finally {
         fin.close();
@@ -264,7 +265,8 @@
         BufferedReader rdr = new BufferedReader(fin);
         Scanner scannerC = new Scanner(rdr);
         number = scannerC.nextLong();
-      } catch (InputMismatchException e) {
+        scannerC.close();
+      } catch (Exception e) {
         // CPU presumably got offline just after we opened file.
       } finally {
         fin.close();
@@ -295,7 +297,8 @@
         long sys = scanner.nextLong();
         runTime = user + nice + sys;
         idleTime = scanner.nextLong();
-      } catch (InputMismatchException e) {
+        scanner.close();
+      } catch (Exception e) {
         Log.e(TAG, "Problems parsing /proc/stat");
         return null;
       } finally {
diff --git a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
index 67290be..c5beec4 100644
--- a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
@@ -35,6 +35,7 @@
 import org.appspot.apprtc.util.LooperExecutor;
 import org.webrtc.DataChannel;
 import org.webrtc.IceCandidate;
+import org.webrtc.Logging;
 import org.webrtc.MediaCodecVideoEncoder;
 import org.webrtc.MediaConstraints;
 import org.webrtc.MediaConstraints.KeyValuePair;
@@ -51,6 +52,7 @@
 import org.webrtc.VideoSource;
 import org.webrtc.VideoTrack;
 
+import java.util.EnumSet;
 import java.util.LinkedList;
 import java.util.Timer;
 import java.util.TimerTask;
@@ -62,6 +64,7 @@
  *
  * <p>All public methods are routed to local looper thread.
  * All PeerConnectionEvents callbacks are invoked from the same looper thread.
+ * This class is a singleton.
  */
 public class PeerConnectionClient {
   public static final String VIDEO_TRACK_ID = "ARDAMSv0";
@@ -89,18 +92,21 @@
   private static final int MAX_VIDEO_HEIGHT = 1280;
   private static final int MAX_VIDEO_FPS = 30;
 
-  private final LooperExecutor executor;
-  private PeerConnectionFactory factory = null;
-  private PeerConnection peerConnection = null;
-  private VideoSource videoSource;
-  private boolean videoCallEnabled = true;
-  private boolean preferIsac = false;
-  private boolean preferH264 = false;
-  private boolean videoSourceStopped = false;
-  private boolean isError = false;
-  private final Timer statsTimer = new Timer();
+  private static final PeerConnectionClient instance = new PeerConnectionClient();
   private final PCObserver pcObserver = new PCObserver();
   private final SDPObserver sdpObserver = new SDPObserver();
+  private final LooperExecutor executor;
+
+  private PeerConnectionFactory factory;
+  private PeerConnection peerConnection;
+  PeerConnectionFactory.Options options = null;
+  private VideoSource videoSource;
+  private boolean videoCallEnabled;
+  private boolean preferIsac;
+  private boolean preferH264;
+  private boolean videoSourceStopped;
+  private boolean isError;
+  private Timer statsTimer;
   private VideoRenderer.Callbacks localRender;
   private VideoRenderer.Callbacks remoteRender;
   private SignalingParameters signalingParameters;
@@ -112,17 +118,17 @@
   // Queued remote ICE candidates are consumed only after both local and
   // remote descriptions are set. Similarly local ICE candidates are sent to
   // remote peer after both local and remote description are set.
-  private LinkedList<IceCandidate> queuedRemoteCandidates = null;
+  private LinkedList<IceCandidate> queuedRemoteCandidates;
   private PeerConnectionEvents events;
   private boolean isInitiator;
-  private SessionDescription localSdp = null; // either offer or answer SDP
-  private MediaStream mediaStream = null;
+  private SessionDescription localSdp; // either offer or answer SDP
+  private MediaStream mediaStream;
   private int numberOfCameras;
-  private VideoCapturerAndroid videoCapturer = null;
+  private VideoCapturerAndroid videoCapturer;
   // enableVideo is set to true if video should be rendered and sent.
-  private boolean renderVideo = true;
-  private VideoTrack localVideoTrack = null;
-  private VideoTrack remoteVideoTrack = null;
+  private boolean renderVideo;
+  private VideoTrack localVideoTrack;
+  private VideoTrack remoteVideoTrack;
 
   /**
    * Peer connection parameters.
@@ -202,8 +208,20 @@
     public void onPeerConnectionError(final String description);
   }
 
-  public PeerConnectionClient() {
+  private PeerConnectionClient() {
     executor = new LooperExecutor();
+    // Looper thread is started once in private ctor and is used for all
+    // peer connection API calls to ensure new peer connection factory is
+    // created on the same thread as previously destroyed factory.
+    executor.requestStart();
+  }
+
+  public static PeerConnectionClient getInstance() {
+    return instance;
+  }
+
+  public void setPeerConnectionFactoryOptions(PeerConnectionFactory.Options options) {
+    this.options = options;
   }
 
   public void createPeerConnectionFactory(
@@ -214,7 +232,22 @@
     this.peerConnectionParameters = peerConnectionParameters;
     this.events = events;
     videoCallEnabled = peerConnectionParameters.videoCallEnabled;
-    executor.requestStart();
+    // Reset variables to initial states.
+    factory = null;
+    peerConnection = null;
+    preferIsac = false;
+    preferH264 = false;
+    videoSourceStopped = false;
+    isError = false;
+    queuedRemoteCandidates = null;
+    localSdp = null; // either offer or answer SDP
+    mediaStream = null;
+    videoCapturer = null;
+    renderVideo = true;
+    localVideoTrack = null;
+    remoteVideoTrack = null;
+    statsTimer = new Timer();
+
     executor.execute(new Runnable() {
       @Override
       public void run() {
@@ -250,7 +283,10 @@
         closeInternal();
       }
     });
-    executor.requestStop();
+  }
+
+  public boolean isVideoCallEnabled() {
+    return videoCallEnabled;
   }
 
   private void createPeerConnectionFactoryInternal(
@@ -284,16 +320,13 @@
       events.onPeerConnectionError("Failed to initializeAndroidGlobals");
     }
     factory = new PeerConnectionFactory();
-    configureFactory(factory);
+    if (options != null) {
+      Log.d(TAG, "Factory networkIgnoreMask option: " + options.networkIgnoreMask);
+      factory.setOptions(options);
+    }
     Log.d(TAG, "Peer connection factory created.");
   }
 
-  /**
-   * Hook where tests can provide additional configuration for the factory.
-   */
-  protected void configureFactory(PeerConnectionFactory factory) {
-  }
-
   private void createMediaConstraintsInternal() {
     // Create peer connection constraints.
     pcConstraints = new MediaConstraints();
@@ -384,12 +417,12 @@
         signalingParameters.iceServers, pcConstraints, pcObserver);
     isInitiator = false;
 
-    // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
+    // Set default WebRTC tracing and INFO libjingle logging.
     // NOTE: this _must_ happen while |factory| is alive!
-    // Logging.enableTracing(
-    //     "logcat:",
-    //     EnumSet.of(Logging.TraceLevel.TRACE_ALL),
-    //     Logging.Severity.LS_SENSITIVE);
+    Logging.enableTracing(
+        "logcat:",
+        EnumSet.of(Logging.TraceLevel.TRACE_DEFAULT),
+        Logging.Severity.LS_INFO);
 
     mediaStream = factory.createLocalMediaStream("ARDAMS");
     if (videoCallEnabled) {
@@ -401,6 +434,10 @@
       }
       Log.d(TAG, "Opening camera: " + cameraDeviceName);
       videoCapturer = VideoCapturerAndroid.create(cameraDeviceName);
+      if (videoCapturer == null) {
+        reportError("Failed to open camera");
+        return;
+      }
       mediaStream.addTrack(createVideoTrack(videoCapturer));
     }
 
@@ -419,6 +456,7 @@
       peerConnection.dispose();
       peerConnection = null;
     }
+    Log.d(TAG, "Closing video source.");
     if (videoSource != null) {
       videoSource.dispose();
       videoSource = null;
@@ -428,6 +466,7 @@
       factory.dispose();
       factory = null;
     }
+    options = null;
     Log.d(TAG, "Closing peer connection done.");
     events.onPeerConnectionClosed();
   }
@@ -477,17 +516,21 @@
 
   public void enableStatsEvents(boolean enable, int periodMs) {
     if (enable) {
-      statsTimer.schedule(new TimerTask() {
-        @Override
-        public void run() {
-          executor.execute(new Runnable() {
-            @Override
-            public void run() {
-              getStats();
-            }
-          });
-        }
-      }, 0, periodMs);
+      try {
+        statsTimer.schedule(new TimerTask() {
+          @Override
+          public void run() {
+            executor.execute(new Runnable() {
+              @Override
+              public void run() {
+                getStats();
+              }
+            });
+          }
+        }, 0, periodMs);
+      } catch (Exception e) {
+        Log.e(TAG, "Can not schedule statistics timer", e);
+      }
     } else {
       statsTimer.cancel();
     }
@@ -769,8 +812,10 @@
   }
 
   private void switchCameraInternal() {
-    if (!videoCallEnabled || numberOfCameras < 2) {
-      return;  // No video is sent or only one camera is available.
+    if (!videoCallEnabled || numberOfCameras < 2 || isError || videoCapturer == null) {
+      Log.e(TAG, "Failed to switch camera. Video: " + videoCallEnabled + ". Error : "
+          + isError + ". Number of cameras: " + numberOfCameras);
+      return;  // No video is sent or only one camera is available or error happened.
     }
     Log.d(TAG, "Switch camera");
     videoCapturer.switchCamera();
@@ -780,9 +825,7 @@
     executor.execute(new Runnable() {
       @Override
       public void run() {
-        if (peerConnection != null && !isError) {
-          switchCameraInternal();
-        }
+        switchCameraInternal();
       }
     });
   }
diff --git a/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
index 92bfbd7..b14d2d4 100644
--- a/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
+++ b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
@@ -37,7 +37,6 @@
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.webrtc.IceCandidate;
-import org.webrtc.MediaConstraints;
 import org.webrtc.PeerConnection;
 import org.webrtc.SessionDescription;
 
@@ -170,18 +169,8 @@
         }
       }
 
-      MediaConstraints pcConstraints = constraintsFromJSON(roomJson.getString("pc_constraints"));
-      Log.d(TAG, "pcConstraints: " + pcConstraints);
-      MediaConstraints videoConstraints = constraintsFromJSON(
-          getAVConstraints("video", roomJson.getString("media_constraints")));
-      Log.d(TAG, "videoConstraints: " + videoConstraints);
-      MediaConstraints audioConstraints = constraintsFromJSON(
-          getAVConstraints("audio", roomJson.getString("media_constraints")));
-      Log.d(TAG, "audioConstraints: " + audioConstraints);
-
       SignalingParameters params = new SignalingParameters(
           iceServers, initiator,
-          pcConstraints, videoConstraints, audioConstraints,
           clientId, wssUrl, wssPostUrl,
           offerSdp, iceCandidates);
       events.onSignalingParametersReady(params);
@@ -193,59 +182,6 @@
     }
   }
 
-  // Return the constraints specified for |type| of "audio" or "video" in
-  // |mediaConstraintsString|.
-  private String getAVConstraints (
-      String type, String mediaConstraintsString) throws JSONException {
-    JSONObject json = new JSONObject(mediaConstraintsString);
-    // Tricky handling of values that are allowed to be (boolean or
-    // MediaTrackConstraints) by the getUserMedia() spec.  There are three
-    // cases below.
-    if (!json.has(type) || !json.optBoolean(type, true)) {
-      // Case 1: "audio"/"video" is not present, or is an explicit "false"
-      // boolean.
-      return null;
-    }
-    if (json.optBoolean(type, false)) {
-      // Case 2: "audio"/"video" is an explicit "true" boolean.
-      return "{\"mandatory\": {}, \"optional\": []}";
-    }
-    // Case 3: "audio"/"video" is an object.
-    return json.getJSONObject(type).toString();
-  }
-
-  private MediaConstraints constraintsFromJSON(String jsonString)
-      throws JSONException {
-    if (jsonString == null) {
-      return null;
-    }
-    MediaConstraints constraints = new MediaConstraints();
-    JSONObject json = new JSONObject(jsonString);
-    JSONObject mandatoryJSON = json.optJSONObject("mandatory");
-    if (mandatoryJSON != null) {
-      JSONArray mandatoryKeys = mandatoryJSON.names();
-      if (mandatoryKeys != null) {
-        for (int i = 0; i < mandatoryKeys.length(); ++i) {
-          String key = mandatoryKeys.getString(i);
-          String value = mandatoryJSON.getString(key);
-          constraints.mandatory.add(
-              new MediaConstraints.KeyValuePair(key, value));
-        }
-      }
-    }
-    JSONArray optionalJSON = json.optJSONArray("optional");
-    if (optionalJSON != null) {
-      for (int i = 0; i < optionalJSON.length(); ++i) {
-        JSONObject keyValueDict = optionalJSON.getJSONObject(i);
-        String key = keyValueDict.names().getString(0);
-        String value = keyValueDict.getString(key);
-        constraints.optional.add(
-            new MediaConstraints.KeyValuePair(key, value));
-      }
-    }
-    return constraints;
-  }
-
   // Requests & returns a TURN ICE Server based on a request URL.  Must be run
   // off the main thread!
   private LinkedList<PeerConnection.IceServer> requestTurnServers(String url)
diff --git a/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
index c783816..7ba2575 100644
--- a/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
@@ -127,7 +127,7 @@
     this.roomID = roomID;
     this.clientID = clientID;
     if (state != WebSocketConnectionState.CONNECTED) {
-      Log.d(TAG, "WebSocket register() in state " + state);
+      Log.w(TAG, "WebSocket register() in state " + state);
       return;
     }
     Log.d(TAG, "Registering WebSocket for room " + roomID + ". CLientID: " + clientID);
@@ -190,17 +190,16 @@
     checkIfCalledOnValidThread();
     Log.d(TAG, "Disonnect WebSocket. State: " + state);
     if (state == WebSocketConnectionState.REGISTERED) {
+      // Send "bye" to WebSocket server.
       send("{\"type\": \"bye\"}");
       state = WebSocketConnectionState.CONNECTED;
+      // Send http DELETE to http WebSocket server.
+      sendWSSMessage("DELETE", "");
     }
     // Close WebSocket in CONNECTED or ERROR states only.
     if (state == WebSocketConnectionState.CONNECTED
         || state == WebSocketConnectionState.ERROR) {
       ws.disconnect();
-
-      // Send DELETE to http WebSocket server.
-      sendWSSMessage("DELETE", "");
-
       state = WebSocketConnectionState.CLOSED;
 
       // Wait for websocket close event to prevent websocket library from
diff --git a/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java b/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java
index d1fddb4..9cb0196 100644
--- a/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java
+++ b/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java
@@ -45,6 +45,7 @@
   private final String url;
   private final String message;
   private final AsyncHttpEvents events;
+  private String contentType;
 
   /**
    * Http requests callbacks.
@@ -62,6 +63,10 @@
     this.events = events;
   }
 
+  public void setContentType(String contentType) {
+    this.contentType = contentType;
+  }
+
   public void send() {
     Runnable runHttp = new Runnable() {
       public void run() {
@@ -92,8 +97,11 @@
         connection.setDoOutput(true);
         connection.setFixedLengthStreamingMode(postData.length);
       }
-      connection.setRequestProperty(
-          "content-type", "text/plain; charset=utf-8");
+      if (contentType == null) {
+        connection.setRequestProperty("Content-Type", "text/plain; charset=utf-8");
+      } else {
+        connection.setRequestProperty("Content-Type", contentType);
+      }
 
       // Send POST request.
       if (doOutput && postData.length > 0) {
@@ -105,9 +113,9 @@
       // Get response.
       int responseCode = connection.getResponseCode();
       if (responseCode != 200) {
-        connection.disconnect();
         events.onHttpError("Non-200 response to " + method + " to URL: "
             + url + " : " + connection.getHeaderField(null));
+        connection.disconnect();
         return;
       }
       InputStream responseStream = connection.getInputStream();
diff --git a/talk/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java b/talk/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java
index 7354fb8..4806491 100644
--- a/talk/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java
+++ b/talk/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java
@@ -61,7 +61,6 @@
   private static final String VIDEO_CODEC_VP9 = "VP9";
   private static final String VIDEO_CODEC_H264 = "H264";
   private static final int AUDIO_RUN_TIMEOUT = 1000;
-  private static final String DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT = "DtlsSrtpKeyAgreement";
   private static final String LOCAL_RENDERER_NAME = "Local renderer";
   private static final String REMOTE_RENDERER_NAME = "Remote renderer";
 
@@ -130,17 +129,6 @@
     }
   }
 
-  // Test instance of the PeerConnectionClient class that overrides the options
-  // for the factory so we can run the test without an Internet connection.
-  class TestPeerConnectionClient extends PeerConnectionClient {
-    protected void configureFactory(PeerConnectionFactory factory) {
-      PeerConnectionFactory.Options options =
-          new PeerConnectionFactory.Options();
-      options.networkIgnoreMask = 0;
-      factory.setOptions(options);
-    }
-  }
-
   // Peer connection events implementation.
   @Override
   public void onLocalDescription(SessionDescription sdp) {
@@ -251,33 +239,25 @@
     }
   }
 
-  private SignalingParameters getTestSignalingParameters() {
-    List<PeerConnection.IceServer> iceServers =
-        new LinkedList<PeerConnection.IceServer>();
-    MediaConstraints pcConstraints = new MediaConstraints();
-    pcConstraints.optional.add(
-        new MediaConstraints.KeyValuePair(DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT, "false"));
-    MediaConstraints videoConstraints = new MediaConstraints();
-    MediaConstraints audioConstraints = new MediaConstraints();
-    SignalingParameters signalingParameters = new SignalingParameters(
-        iceServers, true,
-        pcConstraints, videoConstraints, audioConstraints,
-        null, null, null,
-        null, null);
-    return signalingParameters;
-  }
-
   PeerConnectionClient createPeerConnectionClient(
       MockRenderer localRenderer, MockRenderer remoteRenderer,
       boolean enableVideo, String videoCodec) {
-    SignalingParameters signalingParameters = getTestSignalingParameters();
+    List<PeerConnection.IceServer> iceServers =
+        new LinkedList<PeerConnection.IceServer>();
+    SignalingParameters signalingParameters = new SignalingParameters(
+        iceServers, true, // iceServers, initiator.
+        null, null, null, // clientId, wssUrl, wssPostUrl.
+        null, null); // offerSdp, iceCandidates.
     PeerConnectionParameters peerConnectionParameters =
         new PeerConnectionParameters(
             enableVideo, true, // videoCallEnabled, loopback.
             0, 0, 0, 0, videoCodec, true, // video codec parameters.
             0, "OPUS", true); // audio codec parameters.
 
-    PeerConnectionClient client = new TestPeerConnectionClient();
+    PeerConnectionClient client = PeerConnectionClient.getInstance();
+    PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
+    options.networkIgnoreMask = 0;
+    client.setPeerConnectionFactoryOptions(options);
     client.createPeerConnectionFactory(
         getInstrumentation().getContext(), null,
         peerConnectionParameters, this);