Add DCHECK to ensure that NetEq's packet buffer is not empty

This DCHECK ensures that one packet was inserted after the buffer was
flushed.

R=kwiberg@webrtc.org

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

git-svn-id: http://webrtc.googlecode.com/svn/trunk@7719 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/talk/examples/android/res/values/strings.xml b/talk/examples/android/res/values/strings.xml
index 774eee1..e96d6bf 100644
--- a/talk/examples/android/res/values/strings.xml
+++ b/talk/examples/android/res/values/strings.xml
@@ -27,11 +27,6 @@
     <string name="pref_room_key">room_preference</string>
     <string name="pref_room_list_key">room_list_preference</string>
 
-    <string name="pref_url_key">url_preference</string>
-    <string name="pref_url_title">Connection URL:</string>
-    <string name="pref_url_dlg">Enter AppRTC connection server URL.</string>
-    <string name="pref_url_default">https://apprtc.appspot.com</string>
-
     <string name="pref_resolution_key">resolution_preference</string>
     <string name="pref_resolution_title">Video resolution.</string>
     <string name="pref_resolution_dlg">Enter AppRTC local video resolution.</string>
diff --git a/talk/examples/android/res/xml/preferences.xml b/talk/examples/android/res/xml/preferences.xml
index b8c08bb..c5c3a1e 100644
--- a/talk/examples/android/res/xml/preferences.xml
+++ b/talk/examples/android/res/xml/preferences.xml
@@ -1,11 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
-    <EditTextPreference
-        android:key="@string/pref_url_key"
-        android:title="@string/pref_url_title"
-        android:defaultValue="@string/pref_url_default"
-        android:inputType="textWebEmailAddress"
-        android:dialogTitle="@string/pref_url_dlg" />
     <ListPreference
         android:key="@string/pref_resolution_key"
         android:title="@string/pref_resolution_title"
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java
index 5c34fca..ddabcd6 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java
@@ -42,10 +42,14 @@
   public void connectToRoom(String url);
 
   /**
-   * Send local SDP (offer or answer, depending on role) to the
-   * other participant.
+   * Send offer SDP to the other participant.
    */
-  public void sendLocalDescription(final SessionDescription sdp);
+  public void sendOfferSdp(final SessionDescription sdp);
+
+  /**
+   * Send answer SDP to the other participant.
+   */
+  public void sendAnswerSdp(final SessionDescription sdp);
 
   /**
    * Send Ice candidate to the other participant.
@@ -60,36 +64,54 @@
   /**
    * Struct holding the signaling parameters of an AppRTC room.
    */
-  public class AppRTCSignalingParameters {
+  public class SignalingParameters {
+    public final boolean websocketSignaling;
     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 postMessageUrl;
+    public final String roomId;
+    public final String clientId;
+    public final String channelToken;
+    public final String offerSdp;
 
-    public AppRTCSignalingParameters(
+    public SignalingParameters(
         List<PeerConnection.IceServer> iceServers,
         boolean initiator, MediaConstraints pcConstraints,
-        MediaConstraints videoConstraints, MediaConstraints audioConstraints) {
+        MediaConstraints videoConstraints, MediaConstraints audioConstraints,
+        String postMessageUrl, String roomId, String clientId,
+        String channelToken, String offerSdp ) {
       this.iceServers = iceServers;
       this.initiator = initiator;
       this.pcConstraints = pcConstraints;
       this.videoConstraints = videoConstraints;
       this.audioConstraints = audioConstraints;
+      this.postMessageUrl = postMessageUrl;
+      this.roomId = roomId;
+      this.clientId = clientId;
+      this.channelToken = channelToken;
+      this.offerSdp = offerSdp;
+      if (channelToken == null || channelToken.length() == 0) {
+        this.websocketSignaling = true;
+      } else {
+        this.websocketSignaling = false;
+      }
     }
   }
 
   /**
-   * Callback interface for messages delivered on signalling channel.
+   * Callback interface for messages delivered on signaling channel.
    *
    * Methods are guaranteed to be invoked on the UI thread of |activity|.
    */
-  public static interface AppRTCSignalingEvents {
+  public static interface SignalingEvents {
     /**
      * Callback fired once the room's signaling parameters
-     * AppRTCSignalingParameters are extracted.
+     * SignalingParameters are extracted.
      */
-    public void onConnectedToRoom(final AppRTCSignalingParameters params);
+    public void onConnectedToRoom(final SignalingParameters params);
 
     /**
      * Callback fired once channel for signaling messages is opened and
@@ -115,6 +137,6 @@
     /**
      * Callback fired once channel error happened.
      */
-    public void onChannelError(int code, String description);
+    public void onChannelError(final String description);
   }
 }
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
index 3ad26af..7facd3c 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
@@ -49,7 +49,7 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
-import org.appspot.apprtc.AppRTCClient.AppRTCSignalingParameters;
+import org.appspot.apprtc.AppRTCClient.SignalingParameters;
 import org.webrtc.IceCandidate;
 import org.webrtc.PeerConnectionFactory;
 import org.webrtc.SessionDescription;
@@ -60,17 +60,18 @@
 import org.webrtc.VideoRendererGui.ScalingType;
 
 /**
- * Main Activity of the AppRTCDemo Android app demonstrating interoperability
+ * Activity of the AppRTCDemo Android app demonstrating interoperability
  * between the Android/Java implementation of PeerConnection and the
  * apprtc.appspot.com demo webapp.
  */
 public class AppRTCDemoActivity extends Activity
-    implements AppRTCClient.AppRTCSignalingEvents,
+    implements AppRTCClient.SignalingEvents,
       PeerConnectionClient.PeerConnectionEvents {
   private static final String TAG = "AppRTCClient";
+  private final boolean USE_WEBSOCKETS = false;
   private PeerConnectionClient pc;
-  private AppRTCClient appRtcClient = new GAERTCClient(this, this);
-  private AppRTCSignalingParameters appRtcParameters;
+  private AppRTCClient appRtcClient;
+  private SignalingParameters signalingParameters;
   private AppRTCAudioManager audioManager = null;
   private View rootView;
   private View menuBar;
@@ -199,6 +200,11 @@
       if ((room != null && !room.equals("")) ||
           (loopback != null && loopback.equals("loopback"))) {
         logAndToast(getString(R.string.connecting_to, url));
+        if (USE_WEBSOCKETS) {
+          appRtcClient = new WebSocketRTCClient(this);
+        } else {
+          appRtcClient = new GAERTCClient(this, this);
+        }
         appRtcClient.connectToRoom(url.toString());
         if (room != null && !room.equals("")) {
           roomName.setText(room);
@@ -324,7 +330,7 @@
     finish();
   }
 
-  private void disconnectWithMessage(String errorMessage) {
+  private void disconnectWithMessage(final String errorMessage) {
     new AlertDialog.Builder(this)
     .setTitle(getText(R.string.channel_error_title))
     .setMessage(errorMessage)
@@ -357,20 +363,20 @@
   // -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
   // All events are called from UI thread.
   @Override
-  public void onConnectedToRoom(final AppRTCSignalingParameters params) {
+  public void onConnectedToRoom(final SignalingParameters params) {
     if (audioManager != null) {
       // Store existing audio settings and change audio mode to
       // MODE_IN_COMMUNICATION for best possible VoIP performance.
       logAndToast("Initializing the audio manager...");
       audioManager.init();
     }
-    appRtcParameters = params;
+    signalingParameters = params;
     abortUnless(PeerConnectionFactory.initializeAndroidGlobals(
       this, true, true, VideoRendererGui.getEGLContext()),
         "Failed to initializeAndroidGlobals");
     logAndToast("Creating peer connection...");
     pc = new PeerConnectionClient(
-        this, localRender, remoteRender, appRtcParameters, this);
+        this, localRender, remoteRender, signalingParameters, this);
     if (pc.isHDVideo()) {
       setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
     } else {
@@ -417,7 +423,7 @@
     if (pc == null) {
       return;
     }
-    if (appRtcParameters.initiator) {
+    if (signalingParameters.initiator) {
       logAndToast("Creating OFFER...");
       // Create offer. Offer SDP will be sent to answering client in
       // PeerConnectionEvents.onLocalDescription event.
@@ -432,7 +438,7 @@
     }
     logAndToast("Received remote " + sdp.type + " ...");
     pc.setRemoteDescription(sdp);
-    if (!appRtcParameters.initiator) {
+    if (!signalingParameters.initiator) {
       logAndToast("Creating ANSWER...");
       // Create answer. Answer SDP will be sent to offering client in
       // PeerConnectionEvents.onLocalDescription event.
@@ -454,7 +460,7 @@
   }
 
   @Override
-  public void onChannelError(int code, String description) {
+  public void onChannelError(final String description) {
     disconnectWithMessage(description);
   }
 
@@ -465,7 +471,11 @@
   public void onLocalDescription(final SessionDescription sdp) {
     if (appRtcClient != null) {
       logAndToast("Sending " + sdp.type + " ...");
-      appRtcClient.sendLocalDescription(sdp);
+      if (signalingParameters.initiator) {
+        appRtcClient.sendOfferSdp(sdp);
+      } else {
+        appRtcClient.sendAnswerSdp(sdp);
+      }
     }
   }
 
diff --git a/talk/examples/android/src/org/appspot/apprtc/ConnectActivity.java b/talk/examples/android/src/org/appspot/apprtc/ConnectActivity.java
index ce99bbf..ff78ebc 100644
--- a/talk/examples/android/src/org/appspot/apprtc/ConnectActivity.java
+++ b/talk/examples/android/src/org/appspot/apprtc/ConnectActivity.java
@@ -63,6 +63,10 @@
 public class ConnectActivity extends Activity {
 
   private static final String TAG = "ConnectActivity";
+  private final boolean USE_WEBSOCKETS = false;
+  private final String APPRTC_SERVER = "https://apprtc.appspot.com";
+  private final String APPRTC_WS_SERVER = "https://8-dot-apprtc.appspot.com";
+
   private ImageButton addRoomButton;
   private ImageButton removeRoomButton;
   private ImageButton connectButton;
@@ -70,7 +74,6 @@
   private EditText roomEditText;
   private ListView roomListView;
   private SharedPreferences sharedPref;
-  private String keyprefUrl;
   private String keyprefResolution;
   private String keyprefFps;
   private String keyprefCpuUsageDetection;
@@ -86,7 +89,6 @@
     // Get setting keys.
     PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
     sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
-    keyprefUrl = getString(R.string.pref_url_key);
     keyprefResolution = getString(R.string.pref_resolution_key);
     keyprefFps = getString(R.string.pref_fps_key);
     keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_key);
@@ -193,9 +195,11 @@
       if (view.getId() == R.id.connect_loopback_button) {
         loopback = true;
       }
-      String url = sharedPref.getString(keyprefUrl,
-          getString(R.string.pref_url_default));
-      if (loopback) {
+      String url = APPRTC_SERVER;
+      if (USE_WEBSOCKETS) {
+        url = APPRTC_WS_SERVER;
+      }
+      if (loopback && !USE_WEBSOCKETS) {
         url += "/?debug=loopback";
       } else {
         String roomName = getSelectedItem();
diff --git a/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java b/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java
index 5fd0a54..d44975b 100644
--- a/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java
@@ -141,6 +141,8 @@
 
     @JavascriptInterface
     public void onError(final int code, final String description) {
+      Log.e(TAG, "Channel error. Code: " + code +
+          ". Description: " + description);
       if (!disconnected) {
         handler.onError(code, description);
       }
diff --git a/talk/examples/android/src/org/appspot/apprtc/GAERTCClient.java b/talk/examples/android/src/org/appspot/apprtc/GAERTCClient.java
index c3d9564..2a1e8a1 100644
--- a/talk/examples/android/src/org/appspot/apprtc/GAERTCClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/GAERTCClient.java
@@ -30,20 +30,16 @@
 import android.os.AsyncTask;
 import android.util.Log;
 
-import org.json.JSONArray;
+import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.webrtc.IceCandidate;
-import org.webrtc.MediaConstraints;
-import org.webrtc.PeerConnection;
 import org.webrtc.SessionDescription;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.URL;
 import java.net.URLConnection;
 import java.util.LinkedList;
-import java.util.Scanner;
 
 /**
  * Negotiates signaling for chatting with apprtc.appspot.com "rooms".
@@ -55,25 +51,23 @@
  * Messages to other party (with local Ice candidates and SDP) can
  * be sent after GAE channel is opened and onChannelOpen() callback is invoked.
  */
-public class GAERTCClient implements AppRTCClient {
+public class GAERTCClient implements AppRTCClient, RoomParametersFetcherEvents {
   private static final String TAG = "GAERTCClient";
   private GAEChannelClient channelClient;
   private final Activity activity;
-  private AppRTCClient.AppRTCSignalingEvents events;
-  private final GAEChannelClient.GAEMessageHandler gaeHandler =
-      new GAEHandler();
-  private AppRTCClient.AppRTCSignalingParameters appRTCSignalingParameters;
-  private String gaeBaseHref;
-  private String channelToken;
-  private String postMessageUrl;
+  private SignalingEvents events;
+  private GAEChannelClient.GAEMessageHandler gaeHandler;
+  private SignalingParameters signalingParameters;
+  private RoomParametersFetcher fetcher;
   private LinkedList<String> sendQueue = new LinkedList<String>();
 
-  public GAERTCClient(Activity activity,
-      AppRTCClient.AppRTCSignalingEvents events) {
+  public GAERTCClient(Activity activity, SignalingEvents events) {
     this.activity = activity;
     this.events = events;
   }
 
+  // --------------------------------------------------------------------
+  // AppRTCClient interface implementation.
   /**
    * Asynchronously connect to an AppRTC room URL, e.g.
    * https://apprtc.appspot.com/?r=NNN and register message-handling callbacks
@@ -81,7 +75,8 @@
    */
   @Override
   public void connectToRoom(String url) {
-    (new RoomParameterGetter()).execute(url);
+    fetcher = new RoomParametersFetcher(this);
+    fetcher.execute(url);
   }
 
   /**
@@ -94,6 +89,7 @@
       sendMessage("{\"type\": \"bye\"}");
       channelClient.close();
       channelClient = null;
+      gaeHandler = null;
     }
   }
 
@@ -105,9 +101,17 @@
    * we might want to filter elsewhere.
    */
   @Override
-  public void sendLocalDescription(final SessionDescription sdp) {
+  public void sendOfferSdp(final SessionDescription sdp) {
     JSONObject json = new JSONObject();
-    jsonPut(json, "type", sdp.type.canonicalForm());
+    jsonPut(json, "type", "offer");
+    jsonPut(json, "sdp", sdp.description);
+    sendMessage(json.toString());
+  }
+
+  @Override
+  public void sendAnswerSdp(final SessionDescription sdp) {
+    JSONObject json = new JSONObject();
+    jsonPut(json, "type", "answer");
     jsonPut(json, "sdp", sdp.description);
     sendMessage(json.toString());
   }
@@ -125,7 +129,6 @@
     sendMessage(json.toString());
   }
 
-
   // Queue a message for sending to the room's channel and send it if already
   // connected (other wise queued messages are drained when the channel is
   // eventually established).
@@ -145,223 +148,6 @@
     }
   }
 
-  // AsyncTask that converts an AppRTC room URL into the set of signaling
-  // parameters to use with that room.
-  private class RoomParameterGetter
-      extends AsyncTask<String, Void, AppRTCSignalingParameters> {
-    private Exception exception = null;
-
-    @Override
-    protected AppRTCSignalingParameters doInBackground(String... urls) {
-      if (urls.length != 1) {
-        exception = new RuntimeException("Must be called with a single URL");
-        return null;
-      }
-      try {
-        exception = null;
-        return getParametersForRoomUrl(urls[0]);
-      } catch (JSONException e) {
-        exception = e;
-      } catch (IOException e) {
-        exception = e;
-      }
-      return null;
-    }
-
-    @Override
-    protected void onPostExecute(AppRTCSignalingParameters params) {
-      if (exception != null) {
-        Log.e(TAG, "Room connection error: " + exception.toString());
-        events.onChannelError(0, exception.getMessage());
-        return;
-      }
-      channelClient =
-          new GAEChannelClient(activity, channelToken, gaeHandler);
-      synchronized (sendQueue) {
-        appRTCSignalingParameters = params;
-      }
-      requestQueueDrainInBackground();
-      events.onConnectedToRoom(appRTCSignalingParameters);
-    }
-
-    // Fetches |url| and fishes the signaling parameters out of the JSON.
-    private AppRTCSignalingParameters getParametersForRoomUrl(String url)
-        throws IOException, JSONException {
-      url = url + "&t=json";
-      String response = drainStream((new URL(url)).openConnection().getInputStream());
-      Log.d(TAG, "Room response: " + response);
-      JSONObject roomJson = new JSONObject(response);
-
-      if (roomJson.has("error")) {
-        JSONArray errors = roomJson.getJSONArray("error_messages");
-        throw new IOException(errors.toString());
-      }
-
-      gaeBaseHref = url.substring(0, url.indexOf('?'));
-      channelToken = roomJson.getString("token");
-      postMessageUrl = "/message?r=" +
-          roomJson.getString("room_key") + "&u=" +
-          roomJson.getString("me");
-      boolean initiator = roomJson.getInt("initiator") == 1;
-      LinkedList<PeerConnection.IceServer> iceServers =
-          iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
-
-      boolean isTurnPresent = false;
-      for (PeerConnection.IceServer server : iceServers) {
-        Log.d(TAG, "IceServer: " + server);
-        if (server.uri.startsWith("turn:")) {
-          isTurnPresent = true;
-          break;
-        }
-      }
-      if (!isTurnPresent) {
-        PeerConnection.IceServer server =
-            requestTurnServer(roomJson.getString("turn_url"));
-        Log.d(TAG, "TurnServer: " + server);
-        iceServers.add(server);
-      }
-
-      MediaConstraints pcConstraints = constraintsFromJSON(
-          roomJson.getString("pc_constraints"));
-      addDTLSConstraintIfMissing(pcConstraints);
-      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);
-
-      return new AppRTCSignalingParameters(
-          iceServers, initiator,
-          pcConstraints, videoConstraints, audioConstraints);
-    }
-
-    // Mimic Chrome and set DtlsSrtpKeyAgreement to true if not set to false by
-    // the web-app.
-    private void addDTLSConstraintIfMissing(
-        MediaConstraints pcConstraints) {
-      for (MediaConstraints.KeyValuePair pair : pcConstraints.mandatory) {
-        if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
-          return;
-        }
-      }
-      for (MediaConstraints.KeyValuePair pair : pcConstraints.optional) {
-        if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
-          return;
-        }
-      }
-      // DTLS isn't being suppressed (e.g. for debug=loopback calls), so enable
-      // it by default.
-      pcConstraints.optional.add(
-          new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
-    }
-
-    // Return the constraints specified for |type| of "audio" or "video" in
-    // |mediaConstraintsString|.
-    private String getAVConstraints(
-        String type, String mediaConstraintsString) {
-      try {
-        JSONObject json = new JSONObject(mediaConstraintsString);
-        // Tricksy 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();
-      } catch (JSONException e) {
-        throw new RuntimeException(e);
-      }
-    }
-
-    private MediaConstraints constraintsFromJSON(String jsonString) {
-      if (jsonString == null) {
-        return null;
-      }
-      try {
-        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;
-      } catch (JSONException e) {
-        throw new RuntimeException(e);
-      }
-    }
-
-    // Requests & returns a TURN ICE Server based on a request URL.  Must be run
-    // off the main thread!
-    private PeerConnection.IceServer requestTurnServer(String url) {
-      try {
-        URLConnection connection = (new URL(url)).openConnection();
-        connection.addRequestProperty("user-agent", "Mozilla/5.0");
-        connection.addRequestProperty("origin", "https://apprtc.appspot.com");
-        String response = drainStream(connection.getInputStream());
-        JSONObject responseJSON = new JSONObject(response);
-        String uri = responseJSON.getJSONArray("uris").getString(0);
-        String username = responseJSON.getString("username");
-        String password = responseJSON.getString("password");
-        return new PeerConnection.IceServer(uri, username, password);
-      } catch (JSONException e) {
-        throw new RuntimeException(e);
-      } catch (IOException e) {
-        throw new RuntimeException(e);
-      }
-    }
-  }
-
-  // Return the list of ICE servers described by a WebRTCPeerConnection
-  // configuration string.
-  private LinkedList<PeerConnection.IceServer> iceServersFromPCConfigJSON(
-      String pcConfig) {
-    try {
-      JSONObject json = new JSONObject(pcConfig);
-      JSONArray servers = json.getJSONArray("iceServers");
-      LinkedList<PeerConnection.IceServer> ret =
-          new LinkedList<PeerConnection.IceServer>();
-      for (int i = 0; i < servers.length(); ++i) {
-        JSONObject server = servers.getJSONObject(i);
-        String url = server.getString("urls");
-        String credential =
-            server.has("credential") ? server.getString("credential") : "";
-        ret.add(new PeerConnection.IceServer(url, "", credential));
-      }
-      return ret;
-    } catch (JSONException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
   // Request an attempt to drain the send queue, on a background thread.
   private void requestQueueDrainInBackground() {
     (new AsyncTask<Void, Void, Void>() {
@@ -375,37 +161,68 @@
   // Send all queued messages if connected to the room.
   private void maybeDrainQueue() {
     synchronized (sendQueue) {
-      if (appRTCSignalingParameters == null) {
+      if (signalingParameters == null) {
         return;
       }
       try {
         for (String msg : sendQueue) {
           Log.d(TAG, "SEND: " + msg);
-          URLConnection connection =
-              new URL(gaeBaseHref + postMessageUrl).openConnection();
+          URLConnection connection = new URL(
+              signalingParameters.postMessageUrl).openConnection();
           connection.setDoOutput(true);
           connection.getOutputStream().write(msg.getBytes("UTF-8"));
           if (!connection.getHeaderField(null).startsWith("HTTP/1.1 200 ")) {
-            throw new IOException(
-                "Non-200 response to POST: " + connection.getHeaderField(null) +
-                " for msg: " + msg);
+            String errorMessage = "Non-200 response to POST: " +
+                connection.getHeaderField(null) + " for msg: " + msg;
+            reportChannelError(errorMessage);
           }
         }
       } catch (IOException e) {
-        throw new RuntimeException(e);
+        reportChannelError("GAE Post error: " + e.getMessage());
       }
       sendQueue.clear();
     }
   }
 
-  // Return the contents of an InputStream as a String.
-  private static String drainStream(InputStream in) {
-    Scanner s = new Scanner(in).useDelimiter("\\A");
-    return s.hasNext() ? s.next() : "";
+  private void reportChannelError(final String errorMessage) {
+    Log.e(TAG, errorMessage);
+    activity.runOnUiThread(new Runnable() {
+      public void run() {
+        events.onChannelError(errorMessage);
+      }
+    });
   }
 
+  // --------------------------------------------------------------------
+  // RoomConnectionEvents interface implementation.
+  // All events are called on UI thread.
+  @Override
+  public void onSignalingParametersReady(final SignalingParameters params) {
+    Log.d(TAG, "Room signaling parameters ready.");
+    if (params.websocketSignaling) {
+      reportChannelError("Room does not support GAE channel signaling.");
+      return;
+    }
+    gaeHandler = new GAEHandler();
+    channelClient =
+        new GAEChannelClient(activity, params.channelToken, gaeHandler);
+    synchronized (sendQueue) {
+      signalingParameters = params;
+    }
+    requestQueueDrainInBackground();
+    events.onConnectedToRoom(signalingParameters);
+  }
+
+  @Override
+  public void onSignalingParametersError(final String description) {
+    reportChannelError("Room connection error: " + description);
+  }
+
+
+  // --------------------------------------------------------------------
+  // GAEMessageHandler interface implementation.
   // Implementation detail: handler for receiving GAE messages and dispatching
-  // them appropriately.
+  // them appropriately. All dispatched messages are called from UI thread.
   private class GAEHandler implements GAEChannelClient.GAEMessageHandler {
     private boolean channelOpen = false;
 
@@ -442,10 +259,10 @@
             } else if (type.equals("bye")) {
               events.onChannelClose();
             } else {
-              events.onChannelError(1, "Unexpected channel message: " + msg);
+              reportChannelError("Unexpected channel message: " + msg);
             }
           } catch (JSONException e) {
-            events.onChannelError(1, "Channel message JSON parsing error: " +
+            reportChannelError("Channel message JSON parsing error: " +
                 e.toString());
           }
         }
@@ -462,12 +279,9 @@
     }
 
     public void onError(final int code, final String description) {
-      activity.runOnUiThread(new Runnable() {
-        public void run() {
-          events.onChannelError(code, description);
-          channelOpen = false;
-        }
-      });
+      channelOpen = false;
+      reportChannelError("GAE Handler error. Code: " + code +
+          ". " + description);
     }
   }
 
diff --git a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
index 9c917bb..b005de7 100644
--- a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
@@ -30,7 +30,7 @@
 import android.app.Activity;
 import android.util.Log;
 
-import org.appspot.apprtc.AppRTCClient.AppRTCSignalingParameters;
+import org.appspot.apprtc.AppRTCClient.SignalingParameters;
 import org.webrtc.DataChannel;
 import org.webrtc.IceCandidate;
 import org.webrtc.MediaConstraints;
@@ -53,7 +53,7 @@
 import java.util.regex.Pattern;
 
 public class PeerConnectionClient {
-  private static final String TAG = "RTCClient";
+  private static final String TAG = "PCRTCClient";
   private final Activity activity;
   private PeerConnectionFactory factory;
   private PeerConnection pc;
@@ -63,8 +63,11 @@
   private final SDPObserver sdpObserver = new SDPObserver();
   private final VideoRenderer.Callbacks localRender;
   private final VideoRenderer.Callbacks remoteRender;
-  private LinkedList<IceCandidate> queuedRemoteCandidates =
-      new LinkedList<IceCandidate>();
+  // 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> queuedLocalCandidates = null;
   private MediaConstraints sdpMediaConstraints;
   private MediaConstraints videoConstraints;
   private PeerConnectionEvents events;
@@ -77,26 +80,28 @@
       Activity activity,
       VideoRenderer.Callbacks localRender,
       VideoRenderer.Callbacks remoteRender,
-      AppRTCSignalingParameters appRtcParameters,
+      SignalingParameters signalingParameters,
       PeerConnectionEvents events) {
     this.activity = activity;
     this.localRender = localRender;
     this.remoteRender = remoteRender;
     this.events = events;
-    isInitiator = appRtcParameters.initiator;
+    isInitiator = signalingParameters.initiator;
+    queuedRemoteCandidates = new LinkedList<IceCandidate>();
+    queuedLocalCandidates = new LinkedList<IceCandidate>();
 
     sdpMediaConstraints = new MediaConstraints();
     sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
         "OfferToReceiveAudio", "true"));
     sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
         "OfferToReceiveVideo", "true"));
-    videoConstraints = appRtcParameters.videoConstraints;
+    videoConstraints = signalingParameters.videoConstraints;
 
     factory = new PeerConnectionFactory();
-    MediaConstraints pcConstraints = appRtcParameters.pcConstraints;
+    MediaConstraints pcConstraints = signalingParameters.pcConstraints;
     pcConstraints.optional.add(
         new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
-    pc = factory.createPeerConnection(appRtcParameters.iceServers,
+    pc = factory.createPeerConnection(signalingParameters.iceServers,
         pcConstraints, pcObserver);
     isInitiator = false;
 
@@ -113,11 +118,11 @@
       pc.addStream(videoMediaStream);
     }
 
-    if (appRtcParameters.audioConstraints != null) {
+    if (signalingParameters.audioConstraints != null) {
       MediaStream lMS = factory.createLocalMediaStream("ARDAMSAudio");
       lMS.addTrack(factory.createAudioTrack(
           "ARDAMSa0",
-          factory.createAudioSource(appRtcParameters.audioConstraints)));
+          factory.createAudioSource(signalingParameters.audioConstraints)));
       pc.addStream(lMS);
     }
   }
@@ -176,7 +181,6 @@
     });
   }
 
-
   public void addRemoteIceCandidate(final IceCandidate candidate) {
     activity.runOnUiThread(new Runnable() {
       public void run() {
@@ -379,11 +383,21 @@
     return newSdpDescription.toString();
   }
 
-  private void drainRemoteCandidates() {
-    for (IceCandidate candidate : queuedRemoteCandidates) {
-      pc.addIceCandidate(candidate);
+  private void drainCandidates() {
+    if (queuedLocalCandidates != null) {
+      Log.d(TAG, "Send " + queuedLocalCandidates.size() + " local candidates");
+      for (IceCandidate candidate : queuedLocalCandidates) {
+        events.onIceCandidate(candidate);
+      }
+      queuedLocalCandidates = null;
     }
-    queuedRemoteCandidates = null;
+    if (queuedRemoteCandidates != null) {
+      Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates");
+      for (IceCandidate candidate : queuedRemoteCandidates) {
+        pc.addIceCandidate(candidate);
+      }
+      queuedRemoteCandidates = null;
+    }
   }
 
   public void switchCamera() {
@@ -435,7 +449,11 @@
     public void onIceCandidate(final IceCandidate candidate){
       activity.runOnUiThread(new Runnable() {
         public void run() {
-          events.onIceCandidate(candidate);
+          if (queuedLocalCandidates != null) {
+            queuedLocalCandidates.add(candidate);
+          } else {
+            events.onIceCandidate(candidate);
+          }
         }
       });
     }
@@ -470,6 +488,7 @@
     @Override
     public void onIceGatheringChange(
       PeerConnection.IceGatheringState newState) {
+      Log.d(TAG, "IceGatheringState: " + newState);
     }
 
     @Override
@@ -543,20 +562,20 @@
               Log.d(TAG, "Local SDP set succesfully");
               events.onLocalDescription(localSdp);
             } else {
-              // We've just set remote description,
-              // so drain remote ICE candidates.
+              // We've just set remote description, so drain remote
+              // and send local ICE candidates.
               Log.d(TAG, "Remote SDP set succesfully");
-              drainRemoteCandidates();
+              drainCandidates();
             }
           } else {
             // For answering peer connection we set remote SDP and then
             // create answer and set local SDP.
             if (pc.getLocalDescription() != null) {
-              // We've just set our local SDP so time to send it and drain
-              // remote ICE candidates.
+              // We've just set our local SDP so time to send it, drain
+              // remote and send local ICE candidates.
               Log.d(TAG, "Local SDP set succesfully");
               events.onLocalDescription(localSdp);
-              drainRemoteCandidates();
+              drainCandidates();
             } else {
               // We've just set remote SDP - do nothing for now -
               // answer will be created soon.
diff --git a/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
new file mode 100644
index 0000000..99981f3
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
@@ -0,0 +1,296 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.appspot.apprtc;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import org.appspot.apprtc.AppRTCClient.SignalingParameters;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.MediaConstraints;
+import org.webrtc.PeerConnection;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.LinkedList;
+import java.util.Scanner;
+
+// AsyncTask that converts an AppRTC room URL into the set of signaling
+// parameters to use with that room.
+public class RoomParametersFetcher
+    extends AsyncTask<String, Void, SignalingParameters> {
+  private static final String TAG = "RoomRTCClient";
+  private Exception exception = null;
+  private RoomParametersFetcherEvents events = null;
+
+  /**
+   * Room parameters fetcher callbacks.
+   */
+  public static interface RoomParametersFetcherEvents {
+    /**
+     * Callback fired once the room's signaling parameters
+     * SignalingParameters are extracted.
+     */
+    public void onSignalingParametersReady(final SignalingParameters params);
+
+    /**
+     * Callback for room parameters extraction error.
+     */
+    public void onSignalingParametersError(final String description);
+  }
+
+  public RoomParametersFetcher(RoomParametersFetcherEvents events) {
+    super();
+    this.events = events;
+  }
+
+  @Override
+  protected SignalingParameters doInBackground(String... urls) {
+    if (events == null) {
+      exception = new RuntimeException("Room conenction events should be set");
+      return null;
+    }
+    if (urls.length != 1) {
+      exception = new RuntimeException("Must be called with a single URL");
+      return null;
+    }
+    try {
+      exception = null;
+      return getParametersForRoomUrl(urls[0]);
+    } catch (JSONException e) {
+      exception = e;
+    } catch (IOException e) {
+      exception = e;
+    }
+    return null;
+  }
+
+  @Override
+  protected void onPostExecute(SignalingParameters params) {
+    if (exception != null) {
+      Log.e(TAG, "Room connection error: " + exception.toString());
+      events.onSignalingParametersError(exception.getMessage());
+      return;
+    }
+    if (params == null) {
+      Log.e(TAG, "Can not extract room parameters");
+      events.onSignalingParametersError("Can not extract room parameters");
+      return;
+    }
+    events.onSignalingParametersReady(params);
+  }
+
+  // Fetches |url| and fishes the signaling parameters out of the JSON.
+  private SignalingParameters getParametersForRoomUrl(String url)
+      throws IOException, JSONException {
+    url = url + "&t=json";
+    Log.d(TAG, "Connecting to room: " + url);
+    InputStream responseStream = new BufferedInputStream(
+        (new URL(url)).openConnection().getInputStream());
+    String response = drainStream(responseStream);
+    Log.d(TAG, "Room response: " + response);
+    JSONObject roomJson = new JSONObject(response);
+
+    if (roomJson.has("error")) {
+      JSONArray errors = roomJson.getJSONArray("error_messages");
+      throw new IOException(errors.toString());
+    }
+
+    String roomId = roomJson.getString("room_key");
+    String clientId = roomJson.getString("me");
+    Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId);
+    String channelToken = roomJson.optString("token");
+    String offerSdp = roomJson.optString("offer");
+    if (offerSdp != null && offerSdp.length() > 0) {
+      JSONObject offerJson = new JSONObject(offerSdp);
+      offerSdp = offerJson.getString("sdp");
+      Log.d(TAG, "SDP type: " + offerJson.getString("type"));
+    } else {
+      offerSdp = null;
+    }
+
+    String postMessageUrl = url.substring(0, url.indexOf('?'));
+    postMessageUrl += "/message?r=" + roomId + "&u=" + clientId;
+    Log.d(TAG, "Post url: " + postMessageUrl);
+
+    boolean initiator = roomJson.getInt("initiator") == 1;
+    Log.d(TAG, "Initiator: " + initiator);
+
+    LinkedList<PeerConnection.IceServer> iceServers =
+        iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
+    boolean isTurnPresent = false;
+    for (PeerConnection.IceServer server : iceServers) {
+      Log.d(TAG, "IceServer: " + server);
+      if (server.uri.startsWith("turn:")) {
+        isTurnPresent = true;
+        break;
+      }
+    }
+    if (!isTurnPresent) {
+      PeerConnection.IceServer server =
+          requestTurnServer(roomJson.getString("turn_url"));
+      Log.d(TAG, "TurnServer: " + server);
+      iceServers.add(server);
+    }
+
+    MediaConstraints pcConstraints = constraintsFromJSON(
+        roomJson.getString("pc_constraints"));
+    addDTLSConstraintIfMissing(pcConstraints);
+    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);
+
+    return new SignalingParameters(
+        iceServers, initiator,
+        pcConstraints, videoConstraints, audioConstraints,
+        postMessageUrl, roomId, clientId,
+        channelToken, offerSdp);
+  }
+
+  // Mimic Chrome and set DtlsSrtpKeyAgreement to true if not set to false by
+  // the web-app.
+  private void addDTLSConstraintIfMissing(MediaConstraints pcConstraints) {
+    for (MediaConstraints.KeyValuePair pair : pcConstraints.mandatory) {
+      if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
+        return;
+      }
+    }
+    for (MediaConstraints.KeyValuePair pair : pcConstraints.optional) {
+      if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
+        return;
+      }
+    }
+    // DTLS isn't being suppressed (e.g. for debug=loopback calls), so enable
+    // it by default.
+    pcConstraints.optional.add(
+        new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
+  }
+
+  // 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);
+    // Tricksy 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 PeerConnection.IceServer requestTurnServer(String url)
+      throws IOException, JSONException {
+    URLConnection connection = (new URL(url)).openConnection();
+    connection.addRequestProperty("user-agent", "Mozilla/5.0");
+    connection.addRequestProperty("origin", "https://apprtc.appspot.com");
+    String response = drainStream(connection.getInputStream());
+    JSONObject responseJSON = new JSONObject(response);
+    String uri = responseJSON.getJSONArray("uris").getString(0);
+    String username = responseJSON.getString("username");
+    String password = responseJSON.getString("password");
+    return new PeerConnection.IceServer(uri, username, password);
+  }
+
+  // Return the list of ICE servers described by a WebRTCPeerConnection
+  // configuration string.
+  private LinkedList<PeerConnection.IceServer> iceServersFromPCConfigJSON(
+      String pcConfig) throws JSONException {
+    JSONObject json = new JSONObject(pcConfig);
+    JSONArray servers = json.getJSONArray("iceServers");
+    LinkedList<PeerConnection.IceServer> ret =
+        new LinkedList<PeerConnection.IceServer>();
+    for (int i = 0; i < servers.length(); ++i) {
+      JSONObject server = servers.getJSONObject(i);
+      String url = server.getString("urls");
+      String credential =
+          server.has("credential") ? server.getString("credential") : "";
+      ret.add(new PeerConnection.IceServer(url, "", credential));
+    }
+    return ret;
+  }
+
+  // Return the contents of an InputStream as a String.
+  private String drainStream(InputStream in) {
+    Scanner s = new Scanner(in).useDelimiter("\\A");
+    return s.hasNext() ? s.next() : "";
+  }
+
+}
diff --git a/talk/examples/android/src/org/appspot/apprtc/SettingsActivity.java b/talk/examples/android/src/org/appspot/apprtc/SettingsActivity.java
index eccb67e..367c834 100644
--- a/talk/examples/android/src/org/appspot/apprtc/SettingsActivity.java
+++ b/talk/examples/android/src/org/appspot/apprtc/SettingsActivity.java
@@ -36,7 +36,6 @@
 public class SettingsActivity extends Activity
     implements OnSharedPreferenceChangeListener{
   private SettingsFragment settingsFragment;
-  private String keyprefUrl;
   private String keyprefResolution;
   private String keyprefFps;
   private String keyprefCpuUsageDetection;
@@ -44,7 +43,6 @@
   @Override
   protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
-    keyprefUrl = getString(R.string.pref_url_key);
     keyprefResolution = getString(R.string.pref_resolution_key);
     keyprefFps = getString(R.string.pref_fps_key);
     keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_key);
@@ -63,7 +61,6 @@
     SharedPreferences sharedPreferences =
         settingsFragment.getPreferenceScreen().getSharedPreferences();
     sharedPreferences.registerOnSharedPreferenceChangeListener(this);
-    updateSummary(sharedPreferences, keyprefUrl);
     updateSummary(sharedPreferences, keyprefResolution);
     updateSummary(sharedPreferences, keyprefFps);
     updateSummaryB(sharedPreferences, keyprefCpuUsageDetection);
@@ -80,8 +77,7 @@
   @Override
   public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
       String key) {
-    if (key.equals(keyprefUrl) || key.equals(keyprefResolution) ||
-        key.equals(keyprefFps)) {
+    if (key.equals(keyprefResolution) || key.equals(keyprefFps)) {
       updateSummary(sharedPreferences, key);
     } else if (key.equals(keyprefCpuUsageDetection)) {
       updateSummaryB(sharedPreferences, key);
diff --git a/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
new file mode 100644
index 0000000..373480d
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
@@ -0,0 +1,216 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.appspot.apprtc;
+
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import de.tavendo.autobahn.WebSocketConnection;
+import de.tavendo.autobahn.WebSocketException;
+import de.tavendo.autobahn.WebSocket.WebSocketConnectionObserver;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * WebSocket client implementation.
+ * For proper synchronization all methods should be called from UI thread
+ * and all WebSocket events are delivered on UI thread as well.
+ */
+
+public class WebSocketChannelClient {
+  private final String TAG = "WSChannelRTCClient";
+  private final WebSocketChannelEvents events;
+  private final Handler uiHandler;
+  private WebSocketConnection ws;
+  private WebSocketObserver wsObserver;
+  private URI serverURI;
+  private WebSocketConnectionState state;
+
+  public enum WebSocketConnectionState {
+    NEW, CONNECTED, REGISTERED, CLOSED, ERROR
+  };
+
+  /**
+   * Callback interface for messages delivered on WebSocket.
+   * All events are invoked from UI thread.
+   */
+  public interface WebSocketChannelEvents {
+    public void onWebSocketOpen();
+    public void onWebSocketMessage(final String message);
+    public void onWebSocketClose();
+    public void onWebSocketError(final String description);
+  }
+
+  public WebSocketChannelClient(WebSocketChannelEvents events) {
+    this.events = events;
+    uiHandler = new Handler(Looper.getMainLooper());
+    state = WebSocketConnectionState.NEW;
+  }
+
+  public WebSocketConnectionState getState() {
+    return state;
+  }
+
+  public void connect(String url) {
+    if (state != WebSocketConnectionState.NEW) {
+      Log.e(TAG, "WebSocket is already connected.");
+      return;
+    }
+    Log.d(TAG, "Connecting WebSocket to: " + url);
+
+    ws = new WebSocketConnection();
+    wsObserver = new WebSocketObserver();
+    try {
+      serverURI = new URI(url);
+      ws.connect(serverURI, wsObserver);
+    } catch (URISyntaxException e) {
+      reportError("URI error: " + e.getMessage());
+    } catch (WebSocketException e) {
+      reportError("WebSocket connection error: " + e.getMessage());
+    }
+  }
+
+  public void register(String roomId, String clientId) {
+    if (state != WebSocketConnectionState.CONNECTED) {
+      Log.w(TAG, "WebSocket register() in state " + state);
+      return;
+    }
+    JSONObject json = new JSONObject();
+    try {
+      json.put("cmd", "register");
+      json.put("roomid", roomId);
+      json.put("clientid", clientId);
+      Log.d(TAG, "WS SEND: " + json.toString());
+      ws.sendTextMessage(json.toString());
+      state = WebSocketConnectionState.REGISTERED;
+    } catch (JSONException e) {
+      reportError("WebSocket register JSON error: " + e.getMessage());
+    }
+  }
+
+  public void send(String message) {
+    if (state != WebSocketConnectionState.REGISTERED) {
+      Log.e(TAG, "WebSocket send() in non registered state : " + message);
+      return;
+    }
+    JSONObject json = new JSONObject();
+    try {
+      json.put("cmd", "send");
+      json.put("msg", message);
+      message = json.toString();
+      Log.d(TAG, "WS SEND: " + message);
+      ws.sendTextMessage(message);
+    } catch (JSONException e) {
+      reportError("WebSocket send JSON error: " + e.getMessage());
+    }
+  }
+
+  public void disconnect() {
+    Log.d(TAG, "Disonnect WebSocket. State: " + state);
+    if (state == WebSocketConnectionState.REGISTERED) {
+      send("{\"type\": \"bye\"}");
+      state = WebSocketConnectionState.CONNECTED;
+    }
+    // TODO(glaznev): send DELETE to http WebSocket server once send()
+    // will switch to http POST.
+
+    // Close WebSocket in CONNECTED or ERROR states only.
+    if (state == WebSocketConnectionState.CONNECTED ||
+        state == WebSocketConnectionState.ERROR) {
+      state = WebSocketConnectionState.CLOSED;
+      ws.disconnect();
+    }
+  }
+
+  private void reportError(final String errorMessage) {
+    Log.e(TAG, errorMessage);
+    uiHandler.post(new Runnable() {
+      public void run() {
+        if (state != WebSocketConnectionState.ERROR) {
+          state = WebSocketConnectionState.ERROR;
+          events.onWebSocketError(errorMessage);
+        }
+      }
+    });
+  }
+
+  private class WebSocketObserver implements WebSocketConnectionObserver {
+    @Override
+    public void onOpen() {
+      Log.d(TAG, "WebSocket connection opened to: " + serverURI.toString());
+      uiHandler.post(new Runnable() {
+        public void run() {
+          state = WebSocketConnectionState.CONNECTED;
+          events.onWebSocketOpen();
+        }
+      });
+    }
+
+    @Override
+    public void onClose(WebSocketCloseNotification code, String reason) {
+      Log.d(TAG, "WebSocket connection closed. Code: " + code +
+          ". Reason: " + reason);
+      uiHandler.post(new Runnable() {
+        public void run() {
+          if (state != WebSocketConnectionState.CLOSED) {
+            state = WebSocketConnectionState.CLOSED;
+            events.onWebSocketClose();
+          }
+        }
+      });
+    }
+
+    @Override
+    public void onTextMessage(String payload) {
+      Log.d(TAG, "WS GET: " + payload);
+      final String message = payload;
+      uiHandler.post(new Runnable() {
+        public void run() {
+          if (state == WebSocketConnectionState.CONNECTED ||
+              state == WebSocketConnectionState.REGISTERED) {
+            events.onWebSocketMessage(message);
+          }
+        }
+      });
+    }
+
+    @Override
+    public void onRawTextMessage(byte[] payload) {
+    }
+
+    @Override
+    public void onBinaryMessage(byte[] payload) {
+    }
+  }
+
+}
diff --git a/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java b/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java
new file mode 100644
index 0000000..aaef09b
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java
@@ -0,0 +1,313 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.appspot.apprtc;
+
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.LinkedList;
+
+import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents;
+import org.appspot.apprtc.WebSocketChannelClient.WebSocketChannelEvents;
+import org.appspot.apprtc.WebSocketChannelClient.WebSocketConnectionState;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.IceCandidate;
+import org.webrtc.SessionDescription;
+
+/**
+ * Negotiates signaling for chatting with apprtc.appspot.com "rooms".
+ * Uses the client<->server specifics of the apprtc AppEngine webapp.
+ *
+ * To use: create an instance of this object (registering a message handler) and
+ * call connectToRoom().  Once room connection is established
+ * onConnectedToRoom() callback with room parameters is invoked.
+ * Messages to other party (with local Ice candidates and answer SDP) can
+ * be sent after WebSocket connection is established.
+ */
+public class WebSocketRTCClient implements AppRTCClient,
+    RoomParametersFetcherEvents, WebSocketChannelEvents {
+  private static final String TAG = "WSRTCClient";
+  private static final String WSS_SERVER = "wss://apprtc-ws.webrtc.org:8089/ws";
+
+  private enum ConnectionState {
+    NEW, CONNECTED, CLOSED, ERROR
+  };
+  private final Handler uiHandler;
+  private SignalingEvents events;
+  private SignalingParameters signalingParameters;
+  private WebSocketChannelClient wsClient;
+  private RoomParametersFetcher fetcher;
+  private ConnectionState roomState;
+  private LinkedList<String> gaePostQueue;
+
+  public WebSocketRTCClient(SignalingEvents events) {
+    this.events = events;
+    uiHandler = new Handler(Looper.getMainLooper());
+    gaePostQueue = new LinkedList<String>();
+  }
+
+  // --------------------------------------------------------------------
+  // RoomConnectionEvents interface implementation.
+  // All events are called on UI thread.
+  @Override
+  public void onSignalingParametersReady(final SignalingParameters params) {
+    Log.d(TAG, "Room connection completed.");
+    if (!params.initiator && params.offerSdp == null) {
+      reportError("Offer SDP is not available");
+      return;
+    }
+    signalingParameters = params;
+    roomState = ConnectionState.CONNECTED;
+    events.onConnectedToRoom(signalingParameters);
+    wsClient.register(signalingParameters.roomId, signalingParameters.clientId);
+    events.onChannelOpen();
+    if (!signalingParameters.initiator) {
+      // For call receiver get sdp offer from room parameters.
+      SessionDescription sdp = new SessionDescription(
+          SessionDescription.Type.fromCanonicalForm("offer"),
+          signalingParameters.offerSdp);
+      events.onRemoteDescription(sdp);
+    }
+  }
+
+  @Override
+  public void onSignalingParametersError(final String description) {
+    reportError("Room connection error: " + description);
+  }
+
+  // --------------------------------------------------------------------
+  // WebSocketChannelEvents interface implementation.
+  // All events are called on UI thread.
+  @Override
+  public void onWebSocketOpen() {
+    Log.d(TAG, "Websocket connection completed.");
+    if (roomState == ConnectionState.CONNECTED) {
+      wsClient.register(
+          signalingParameters.roomId, signalingParameters.clientId);
+    }
+  }
+
+  @Override
+  public void onWebSocketMessage(final String msg) {
+    if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
+      Log.e(TAG, "Got WebSocket message in non registered state.");
+      return;
+    }
+    try {
+      JSONObject json = new JSONObject(msg);
+      String msgText = json.getString("msg");
+      String errorText = json.optString("error");
+      if (msgText.length() > 0) {
+        json = new JSONObject(msgText);
+        String type = json.optString("type");
+        if (type.equals("candidate")) {
+          IceCandidate candidate = new IceCandidate(
+              (String) json.get("id"),
+              json.getInt("label"),
+              (String) json.get("candidate"));
+          events.onRemoteIceCandidate(candidate);
+        } else if (type.equals("answer")) {
+          SessionDescription sdp = new SessionDescription(
+              SessionDescription.Type.fromCanonicalForm(type),
+              (String)json.get("sdp"));
+          events.onRemoteDescription(sdp);
+        } else if (type.equals("bye")) {
+          events.onChannelClose();
+        } else {
+          reportError("Unexpected WebSocket message: " + msg);
+        }
+      }
+      else {
+        if (errorText != null && errorText.length() > 0) {
+          reportError("WebSocket error message: " + errorText);
+        } else {
+          reportError("Unexpected WebSocket message: " + msg);
+        }
+      }
+    } catch (JSONException e) {
+      reportError("WebSocket message JSON parsing error: " + e.toString());
+    }
+  }
+
+  @Override
+  public void onWebSocketClose() {
+    events.onChannelClose();
+  }
+
+  @Override
+  public void onWebSocketError(String description) {
+    reportError("WebSocket error: " + description);
+  }
+
+  // --------------------------------------------------------------------
+  // AppRTCClient interface implementation.
+  // Asynchronously connect to an AppRTC room URL, e.g.
+  // https://apprtc.appspot.com/?r=NNN, retrieve room parameters
+  // and connect to WebSocket server.
+  @Override
+  public void connectToRoom(String url) {
+    // Get room parameters.
+    roomState = ConnectionState.NEW;
+    fetcher = new RoomParametersFetcher(this);
+    fetcher.execute(url);
+    // Connect to WebSocket server.
+    wsClient = new WebSocketChannelClient(this);
+    wsClient.connect(WSS_SERVER);
+  }
+
+  @Override
+  public void disconnect() {
+    Log.d(TAG, "Disconnect. Room state: " + roomState);
+    if (roomState == ConnectionState.CONNECTED) {
+      Log.d(TAG, "Closing room.");
+      sendGAEMessage("{\"type\": \"bye\"}");
+    }
+    wsClient.disconnect();
+  }
+
+  // Send local SDP (offer or answer, depending on role) to the
+  // other participant.  Note that it is important to send the output of
+  // create{Offer,Answer} and not merely the current value of
+  // getLocalDescription() because the latter may include ICE candidates that
+  // we might want to filter elsewhere.
+  @Override
+  public void sendOfferSdp(final SessionDescription sdp) {
+    JSONObject json = new JSONObject();
+    jsonPut(json, "sdp", sdp.description);
+    jsonPut(json, "type", "offer");
+    sendGAEMessage(json.toString());
+  }
+
+  @Override
+  public void sendAnswerSdp(final SessionDescription sdp) {
+    if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
+      reportError("Sending answer SDP in non registered state.");
+      return;
+    }
+    JSONObject json = new JSONObject();
+    jsonPut(json, "sdp", sdp.description);
+    jsonPut(json, "type", "answer");
+    wsClient.send(json.toString());
+  }
+
+  // Send Ice candidate to the other participant.
+  @Override
+  public void sendLocalIceCandidate(final IceCandidate candidate) {
+    if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
+      reportError("Sending ICE candidate in non registered state.");
+      return;
+    }
+    JSONObject json = new JSONObject();
+    jsonPut(json, "type", "candidate");
+    jsonPut(json, "label", candidate.sdpMLineIndex);
+    jsonPut(json, "id", candidate.sdpMid);
+    jsonPut(json, "candidate", candidate.sdp);
+    wsClient.send(json.toString());
+  }
+
+  // --------------------------------------------------------------------
+  // Helper functions.
+  private void reportError(final String errorMessage) {
+    Log.e(TAG, errorMessage);
+    uiHandler.post(new Runnable() {
+      public void run() {
+        if (roomState != ConnectionState.ERROR) {
+          roomState = ConnectionState.ERROR;
+          events.onChannelError(errorMessage);
+        }
+      }
+    });
+  }
+
+  // Put a |key|->|value| mapping in |json|.
+  private static void jsonPut(JSONObject json, String key, Object value) {
+    try {
+      json.put(key, value);
+    } catch (JSONException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  // Queue a message for sending to the room  and send it if already connected.
+  private synchronized void sendGAEMessage(String msg) {
+    synchronized (gaePostQueue) {
+      gaePostQueue.add(msg);
+    }
+    (new AsyncTask<Void, Void, Void>() {
+      public Void doInBackground(Void... unused) {
+        maybeDrainGAEPostQueue();
+        return null;
+      }
+    }).execute();
+  }
+
+  // Send all queued messages if connected to the room.
+  private void maybeDrainGAEPostQueue() {
+    synchronized (gaePostQueue) {
+      if (roomState != ConnectionState.CONNECTED) {
+        return;
+      }
+      try {
+        for (String msg : gaePostQueue) {
+          Log.d(TAG, "ROOM SEND: " + msg);
+          // Check if this is 'bye' message and update room connection state.
+          // TODO(glaznev): change this to new bye message format:
+          // https://apprtc.appspot.com/bye/{roomid}/{clientid}
+          JSONObject json = new JSONObject(msg);
+          String type = json.optString("type");
+          if (type != null && type.equals("bye")) {
+            roomState = ConnectionState.CLOSED;
+          }
+          // Send POST request.
+          URLConnection connection = new URL(
+              signalingParameters.postMessageUrl).openConnection();
+          connection.setDoOutput(true);
+          connection.setRequestProperty(
+              "content-type", "text/plain; charset=utf-8");
+          connection.getOutputStream().write(msg.getBytes("UTF-8"));
+          String replyHeader = connection.getHeaderField(null);
+          if (!replyHeader.startsWith("HTTP/1.1 200 ")) {
+            reportError("Non-200 response to POST: " +
+                connection.getHeaderField(null) + " for msg: " + msg);
+          }
+        }
+      } catch (IOException e) {
+        reportError("GAE POST error: " + e.getMessage());
+      } catch (JSONException e) {
+        reportError("GAE POST JSON error: " + e.getMessage());
+      }
+      gaePostQueue.clear();
+    }
+  }
+
+}
diff --git a/talk/examples/android/third_party/autobanh/LICENSE b/talk/examples/android/third_party/autobanh/LICENSE
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/talk/examples/android/third_party/autobanh/LICENSE
@@ -0,0 +1,177 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
diff --git a/talk/examples/android/third_party/autobanh/LICENSE.md b/talk/examples/android/third_party/autobanh/LICENSE.md
new file mode 100644
index 0000000..2079e90
--- /dev/null
+++ b/talk/examples/android/third_party/autobanh/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Cameron Lowell Palmer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/talk/examples/android/third_party/autobanh/autobanh.jar b/talk/examples/android/third_party/autobanh/autobanh.jar
new file mode 100644
index 0000000..5a10b7f
--- /dev/null
+++ b/talk/examples/android/third_party/autobanh/autobanh.jar
Binary files differ
diff --git a/talk/libjingle_examples.gyp b/talk/libjingle_examples.gyp
index f7ce53b..740452d 100755
--- a/talk/libjingle_examples.gyp
+++ b/talk/libjingle_examples.gyp
@@ -314,6 +314,7 @@
                 'examples/android/README',
                 'examples/android/ant.properties',
                 'examples/android/assets/channel.html',
+                'examples/android/third_party/autobanh/autobanh.jar',
                 'examples/android/build.xml',
                 'examples/android/jni/Android.mk',
                 'examples/android/project.properties',
@@ -351,9 +352,12 @@
                 'examples/android/src/org/appspot/apprtc/GAEChannelClient.java',
                 'examples/android/src/org/appspot/apprtc/GAERTCClient.java',
                 'examples/android/src/org/appspot/apprtc/PeerConnectionClient.java',
+                'examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java',
                 'examples/android/src/org/appspot/apprtc/SettingsActivity.java',
                 'examples/android/src/org/appspot/apprtc/SettingsFragment.java',
                 'examples/android/src/org/appspot/apprtc/UnhandledExceptionHandler.java',
+                'examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java',
+                'examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java',
               ],
               'outputs': [
                 '<(PRODUCT_DIR)/AppRTCDemo-debug.apk',
@@ -367,6 +371,7 @@
                 'mkdir -p <(INTERMEDIATE_DIR) && ' # Must happen _before_ the cd below
                 'mkdir -p examples/android/libs/<(android_app_abi) && '
                 'cp <(PRODUCT_DIR)/libjingle_peerconnection.jar examples/android/libs/ &&'
+                'cp examples/android/third_party/autobanh/autobanh.jar examples/android/libs/ &&'
                 '<(android_strip) -o examples/android/libs/<(android_app_abi)/libjingle_peerconnection_so.so  <(PRODUCT_DIR)/libjingle_peerconnection_so.so &&'
                 'cd examples/android && '
                 '{ ANDROID_SDK_ROOT=<(android_sdk_root) '
diff --git a/webrtc/modules/audio_coding/neteq/neteq_impl.cc b/webrtc/modules/audio_coding/neteq/neteq_impl.cc
index f3d1a4f..5ec38d4 100644
--- a/webrtc/modules/audio_coding/neteq/neteq_impl.cc
+++ b/webrtc/modules/audio_coding/neteq/neteq_impl.cc
@@ -15,6 +15,7 @@
 
 #include <algorithm>
 
+#include "webrtc/base/checks.h"
 #include "webrtc/common_audio/signal_processing/include/signal_processing_library.h"
 #include "webrtc/modules/audio_coding/neteq/accelerate.h"
 #include "webrtc/modules/audio_coding/neteq/background_noise.h"
@@ -607,6 +608,8 @@
     new_codec_ = true;
     update_sample_rate_and_channels = true;
     LOG_F(LS_WARNING) << "Packet buffer flushed";
+    DCHECK(!packet_buffer_->Empty())
+        << "One packet must have been inserted after the flush.";
   } else if (ret != PacketBuffer::kOK) {
     LOG_FERR1(LS_WARNING, InsertPacketList, packet_list.size());
     PacketBuffer::DeleteAllPackets(&packet_list);