Update to libjingle 0.6.8.

git-svn-id: http://libjingle.googlecode.com/svn/trunk@103 dd674b97-3498-5ee5-1854-bdd07cd0ff33
diff --git a/CHANGELOG b/CHANGELOG
index 15ac417..fd9bd69 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
 Libjingle
 
+0.6.8 - Dec 22, 2011
+  -
+
 0.6.7 - Dec 21, 2011
   - Release new PeerConnection implementation to app/webrtc.
   - Bug fixes.
diff --git a/talk/app/webrtc/mediastream.h b/talk/app/webrtc/mediastream.h
index 4bb0b6c..5db591a 100644
--- a/talk/app/webrtc/mediastream.h
+++ b/talk/app/webrtc/mediastream.h
@@ -51,7 +51,6 @@
 namespace webrtc {
 
 class AudioDeviceModule;
-class VideoCaptureModule;
 
 // Generic observer interface.
 class ObserverInterface {
diff --git a/talk/app/webrtc/mediastreamtrackproxy.cc b/talk/app/webrtc/mediastreamtrackproxy.cc
index afb222e..15b1d6d 100644
--- a/talk/app/webrtc/mediastreamtrackproxy.cc
+++ b/talk/app/webrtc/mediastreamtrackproxy.cc
@@ -293,7 +293,7 @@
 }
 
 VideoTrackProxy::VideoTrackProxy(const std::string& label,
-                                 VideoCaptureModule* video_device,
+                                 cricket::VideoCapturer* video_device,
                                  talk_base::Thread* signaling_thread)
     : MediaStreamTrackProxy<LocalVideoTrackInterface>(signaling_thread),
       video_track_(VideoTrack::CreateLocal(label, video_device)) {
@@ -318,7 +318,7 @@
 
 talk_base::scoped_refptr<LocalVideoTrackInterface> VideoTrackProxy::CreateLocal(
     const std::string& label,
-    VideoCaptureModule* video_device,
+    cricket::VideoCapturer* video_device,
     talk_base::Thread* signaling_thread) {
   ASSERT(signaling_thread != NULL);
   talk_base::RefCountedObject<VideoTrackProxy>* track =
diff --git a/talk/app/webrtc/mediastreamtrackproxy.h b/talk/app/webrtc/mediastreamtrackproxy.h
index 000f81e..590d6ab 100644
--- a/talk/app/webrtc/mediastreamtrackproxy.h
+++ b/talk/app/webrtc/mediastreamtrackproxy.h
@@ -119,7 +119,7 @@
       talk_base::Thread* signaling_thread);
   static talk_base::scoped_refptr<LocalVideoTrackInterface> CreateLocal(
       const std::string& label,
-      VideoCaptureModule* video_device,
+      cricket::VideoCapturer* video_device,
       talk_base::Thread* signaling_thread);
   static talk_base::scoped_refptr<LocalVideoTrackInterface> CreateLocal(
       LocalVideoTrackInterface* implementation,
@@ -133,7 +133,7 @@
   VideoTrackProxy(const std::string& label,
                   talk_base::Thread* signaling_thread);
   VideoTrackProxy(const std::string& label,
-                  VideoCaptureModule* video_device,
+                  cricket::VideoCapturer* video_device,
                   talk_base::Thread* signaling_thread);
   VideoTrackProxy(LocalVideoTrackInterface* implementation,
                   talk_base::Thread* signaling_thread);
diff --git a/talk/app/webrtc/peerconnection.h b/talk/app/webrtc/peerconnection.h
index 4e2ad0a..6a9b18c 100644
--- a/talk/app/webrtc/peerconnection.h
+++ b/talk/app/webrtc/peerconnection.h
@@ -84,6 +84,8 @@
 }
 
 namespace webrtc {
+class VideoCaptureModule;
+
 // MediaStream container interface.
 class StreamCollectionInterface : public talk_base::RefCountInterface {
  public:
@@ -187,6 +189,12 @@
   ~PeerConnectionInterface() {}
 };
 
+// Helper function to create a new instance of cricket::VideoCapturer
+// from VideoCaptureModule.
+// TODO: This function should be removed once chrome implement video
+// capture as the cricket::VideoCapturer.
+cricket::VideoCapturer* CreateVideoCapturer(VideoCaptureModule* vcm);
+
 // Factory class used for creating cricket::PortAllocator that is used
 // for ICE negotiation.
 class PortAllocatorFactoryInterface : public talk_base::RefCountInterface {
@@ -238,7 +246,7 @@
 
   virtual talk_base::scoped_refptr<LocalVideoTrackInterface>
       CreateLocalVideoTrack(const std::string& label,
-                            VideoCaptureModule* video_device) = 0;
+                            cricket::VideoCapturer* video_device) = 0;
 
   virtual talk_base::scoped_refptr<LocalAudioTrackInterface>
       CreateLocalAudioTrack(const std::string& label,
diff --git a/talk/app/webrtc/peerconnection_unittest.cc b/talk/app/webrtc/peerconnection_unittest.cc
index 6e61dc2..8a264c4 100644
--- a/talk/app/webrtc/peerconnection_unittest.cc
+++ b/talk/app/webrtc/peerconnection_unittest.cc
@@ -224,8 +224,9 @@
   void CreateLocalVideoTrack() {
     fake_video_capture_module_ = FakeVideoCaptureModule::Create(
         talk_base::Thread::Current());
+    // TODO: Use FakeVideoCapturer instead of FakeVideoCaptureModule.
     video_track_ = peer_connection_factory_->CreateLocalVideoTrack(
-        "video_track", fake_video_capture_module_);
+        "video_track", CreateVideoCapturer(fake_video_capture_module_));
   }
 
   int id_;
diff --git a/talk/app/webrtc/peerconnectionfactoryimpl.cc b/talk/app/webrtc/peerconnectionfactoryimpl.cc
index 4275a5f..e74e16e 100644
--- a/talk/app/webrtc/peerconnectionfactoryimpl.cc
+++ b/talk/app/webrtc/peerconnectionfactoryimpl.cc
@@ -220,7 +220,7 @@
 scoped_refptr<LocalVideoTrackInterface>
 PeerConnectionFactory::CreateLocalVideoTrack(
     const std::string& label,
-    VideoCaptureModule* video_device) {
+    cricket::VideoCapturer* video_device) {
   return VideoTrackProxy::CreateLocal(label, video_device,
                                       signaling_thread_);
 }
diff --git a/talk/app/webrtc/peerconnectionfactoryimpl.h b/talk/app/webrtc/peerconnectionfactoryimpl.h
index 9a64f58..ba22d47 100644
--- a/talk/app/webrtc/peerconnectionfactoryimpl.h
+++ b/talk/app/webrtc/peerconnectionfactoryimpl.h
@@ -50,7 +50,7 @@
 
   virtual talk_base::scoped_refptr<LocalVideoTrackInterface>
       CreateLocalVideoTrack(const std::string& label,
-                            VideoCaptureModule* video_device);
+                            cricket::VideoCapturer* video_device);
 
   virtual talk_base::scoped_refptr<LocalAudioTrackInterface>
       CreateLocalAudioTrack(const std::string& label,
diff --git a/talk/app/webrtc/peerconnectionimpl.cc b/talk/app/webrtc/peerconnectionimpl.cc
index d5607e1..fdc2659 100644
--- a/talk/app/webrtc/peerconnectionimpl.cc
+++ b/talk/app/webrtc/peerconnectionimpl.cc
@@ -34,6 +34,7 @@
 #include "talk/base/logging.h"
 #include "talk/base/stringencode.h"
 #include "talk/session/phone/channelmanager.h"
+#include "talk/session/phone/webrtcvideocapturer.h"
 
 namespace {
 
@@ -158,6 +159,16 @@
 
 namespace webrtc {
 
+cricket::VideoCapturer* CreateVideoCapturer(VideoCaptureModule* vcm) {
+  cricket::WebRtcVideoCapturer* video_capturer =
+      new cricket::WebRtcVideoCapturer;
+  if (!video_capturer->Init(vcm)) {
+    delete video_capturer;
+    video_capturer = NULL;
+  }
+  return video_capturer;
+}
+
 PeerConnection::PeerConnection(PeerConnectionFactory* factory)
     : factory_(factory),
       observer_(NULL),
diff --git a/talk/app/webrtc/videotrackimpl.cc b/talk/app/webrtc/videotrackimpl.cc
index 87f797b..0fb218a 100644
--- a/talk/app/webrtc/videotrackimpl.cc
+++ b/talk/app/webrtc/videotrackimpl.cc
@@ -40,13 +40,10 @@
 }
 
 VideoTrack::VideoTrack(const std::string& label,
-                       VideoCaptureModule* video_device)
+                       cricket::VideoCapturer* video_device)
     : MediaStreamTrack<LocalVideoTrackInterface>(label),
       video_device_(NULL) {
-  cricket::WebRtcVideoCapturer* video_device_impl =
-      new cricket::WebRtcVideoCapturer;
-  video_device_impl->Init(video_device);
-  video_device_.reset(video_device_impl);
+  video_device_.reset(video_device);
 }
 
 void VideoTrack::SetRenderer(VideoRendererWrapperInterface* renderer) {
@@ -76,7 +73,7 @@
 
 talk_base::scoped_refptr<VideoTrack> VideoTrack::CreateLocal(
     const std::string& label,
-    VideoCaptureModule* video_device) {
+    cricket::VideoCapturer* video_device) {
   talk_base::RefCountedObject<VideoTrack>* track =
       new talk_base::RefCountedObject<VideoTrack>(label, video_device);
   return track;
diff --git a/talk/app/webrtc/videotrackimpl.h b/talk/app/webrtc/videotrackimpl.h
index a8de715..de99e6a 100644
--- a/talk/app/webrtc/videotrackimpl.h
+++ b/talk/app/webrtc/videotrackimpl.h
@@ -58,7 +58,7 @@
   // Create a video track used for local video tracks.
   static talk_base::scoped_refptr<VideoTrack> CreateLocal(
       const std::string& label,
-      VideoCaptureModule* video_device);
+      cricket::VideoCapturer* video_device);
 
   virtual cricket::VideoCapturer* GetVideoCapture();
   virtual void SetRenderer(VideoRendererWrapperInterface* renderer);
@@ -68,7 +68,7 @@
 
  protected:
   explicit VideoTrack(const std::string& label);
-  VideoTrack(const std::string& label, VideoCaptureModule* video_device);
+  VideoTrack(const std::string& label, cricket::VideoCapturer* video_device);
 
  private:
   talk_base::scoped_ptr<cricket::VideoCapturer> video_device_;
diff --git a/talk/app/webrtcv1/webrtcsession_unittest.cc b/talk/app/webrtcv1/webrtcsession_unittest.cc
index 2c23d6a..c37b0d4 100644
--- a/talk/app/webrtcv1/webrtcsession_unittest.cc
+++ b/talk/app/webrtcv1/webrtcsession_unittest.cc
@@ -29,11 +29,11 @@
 
 #include <list>
 
-#include "base/gunit.h"
-#include "base/helpers.h"
 #include "talk/app/webrtcv1/unittest_utilities.h"
 #include "talk/app/webrtcv1/webrtcsession.h"
 #include "talk/base/fakenetwork.h"
+#include "talk/base/gunit.h"
+#include "talk/base/helpers.h"
 #include "talk/base/scoped_ptr.h"
 #include "talk/base/thread.h"
 #include "talk/p2p/base/fakesession.h"
diff --git a/talk/base/base64_unittest.cc b/talk/base/base64_unittest.cc
new file mode 100644
index 0000000..644a5e0
--- /dev/null
+++ b/talk/base/base64_unittest.cc
@@ -0,0 +1,1004 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include "talk/base/common.h"
+#include "talk/base/base64.h"
+#include "talk/base/gunit.h"
+#include "talk/base/logging.h"
+#include "talk/base/stringutils.h"
+#include "talk/base/stream.h"
+
+#include "talk/base/testbase64.h"
+
+using namespace std;
+using namespace talk_base;
+
+static struct {
+  size_t plain_length;
+  const char* plaintext;
+  const char* cyphertext;
+} base64_tests[] = {
+
+  // Basic bit patterns;
+  // values obtained with "echo -n '...' | uuencode -m test"
+
+  { 1, "\000", "AA==" },
+  { 1, "\001", "AQ==" },
+  { 1, "\002", "Ag==" },
+  { 1, "\004", "BA==" },
+  { 1, "\010", "CA==" },
+  { 1, "\020", "EA==" },
+  { 1, "\040", "IA==" },
+  { 1, "\100", "QA==" },
+  { 1, "\200", "gA==" },
+
+  { 1, "\377", "/w==" },
+  { 1, "\376", "/g==" },
+  { 1, "\375", "/Q==" },
+  { 1, "\373", "+w==" },
+  { 1, "\367", "9w==" },
+  { 1, "\357", "7w==" },
+  { 1, "\337", "3w==" },
+  { 1, "\277", "vw==" },
+  { 1, "\177", "fw==" },
+  { 2, "\000\000", "AAA=" },
+  { 2, "\000\001", "AAE=" },
+  { 2, "\000\002", "AAI=" },
+  { 2, "\000\004", "AAQ=" },
+  { 2, "\000\010", "AAg=" },
+  { 2, "\000\020", "ABA=" },
+  { 2, "\000\040", "ACA=" },
+  { 2, "\000\100", "AEA=" },
+  { 2, "\000\200", "AIA=" },
+  { 2, "\001\000", "AQA=" },
+  { 2, "\002\000", "AgA=" },
+  { 2, "\004\000", "BAA=" },
+  { 2, "\010\000", "CAA=" },
+  { 2, "\020\000", "EAA=" },
+  { 2, "\040\000", "IAA=" },
+  { 2, "\100\000", "QAA=" },
+  { 2, "\200\000", "gAA=" },
+
+  { 2, "\377\377", "//8=" },
+  { 2, "\377\376", "//4=" },
+  { 2, "\377\375", "//0=" },
+  { 2, "\377\373", "//s=" },
+  { 2, "\377\367", "//c=" },
+  { 2, "\377\357", "/+8=" },
+  { 2, "\377\337", "/98=" },
+  { 2, "\377\277", "/78=" },
+  { 2, "\377\177", "/38=" },
+  { 2, "\376\377", "/v8=" },
+  { 2, "\375\377", "/f8=" },
+  { 2, "\373\377", "+/8=" },
+  { 2, "\367\377", "9/8=" },
+  { 2, "\357\377", "7/8=" },
+  { 2, "\337\377", "3/8=" },
+  { 2, "\277\377", "v/8=" },
+  { 2, "\177\377", "f/8=" },
+
+  { 3, "\000\000\000", "AAAA" },
+  { 3, "\000\000\001", "AAAB" },
+  { 3, "\000\000\002", "AAAC" },
+  { 3, "\000\000\004", "AAAE" },
+  { 3, "\000\000\010", "AAAI" },
+  { 3, "\000\000\020", "AAAQ" },
+  { 3, "\000\000\040", "AAAg" },
+  { 3, "\000\000\100", "AABA" },
+  { 3, "\000\000\200", "AACA" },
+  { 3, "\000\001\000", "AAEA" },
+  { 3, "\000\002\000", "AAIA" },
+  { 3, "\000\004\000", "AAQA" },
+  { 3, "\000\010\000", "AAgA" },
+  { 3, "\000\020\000", "ABAA" },
+  { 3, "\000\040\000", "ACAA" },
+  { 3, "\000\100\000", "AEAA" },
+  { 3, "\000\200\000", "AIAA" },
+  { 3, "\001\000\000", "AQAA" },
+  { 3, "\002\000\000", "AgAA" },
+  { 3, "\004\000\000", "BAAA" },
+  { 3, "\010\000\000", "CAAA" },
+  { 3, "\020\000\000", "EAAA" },
+  { 3, "\040\000\000", "IAAA" },
+  { 3, "\100\000\000", "QAAA" },
+  { 3, "\200\000\000", "gAAA" },
+
+  { 3, "\377\377\377", "////" },
+  { 3, "\377\377\376", "///+" },
+  { 3, "\377\377\375", "///9" },
+  { 3, "\377\377\373", "///7" },
+  { 3, "\377\377\367", "///3" },
+  { 3, "\377\377\357", "///v" },
+  { 3, "\377\377\337", "///f" },
+  { 3, "\377\377\277", "//+/" },
+  { 3, "\377\377\177", "//9/" },
+  { 3, "\377\376\377", "//7/" },
+  { 3, "\377\375\377", "//3/" },
+  { 3, "\377\373\377", "//v/" },
+  { 3, "\377\367\377", "//f/" },
+  { 3, "\377\357\377", "/+//" },
+  { 3, "\377\337\377", "/9//" },
+  { 3, "\377\277\377", "/7//" },
+  { 3, "\377\177\377", "/3//" },
+  { 3, "\376\377\377", "/v//" },
+  { 3, "\375\377\377", "/f//" },
+  { 3, "\373\377\377", "+///" },
+  { 3, "\367\377\377", "9///" },
+  { 3, "\357\377\377", "7///" },
+  { 3, "\337\377\377", "3///" },
+  { 3, "\277\377\377", "v///" },
+  { 3, "\177\377\377", "f///" },
+
+  // Random numbers: values obtained with
+  //
+  //  #! /bin/bash
+  //  dd bs=$1 count=1 if=/dev/random of=/tmp/bar.random
+  //  od -N $1 -t o1 /tmp/bar.random
+  //  uuencode -m test < /tmp/bar.random
+  //
+  // where $1 is the number of bytes (2, 3)
+
+  { 2, "\243\361", "o/E=" },
+  { 2, "\024\167", "FHc=" },
+  { 2, "\313\252", "y6o=" },
+  { 2, "\046\041", "JiE=" },
+  { 2, "\145\236", "ZZ4=" },
+  { 2, "\254\325", "rNU=" },
+  { 2, "\061\330", "Mdg=" },
+  { 2, "\245\032", "pRo=" },
+  { 2, "\006\000", "BgA=" },
+  { 2, "\375\131", "/Vk=" },
+  { 2, "\303\210", "w4g=" },
+  { 2, "\040\037", "IB8=" },
+  { 2, "\261\372", "sfo=" },
+  { 2, "\335\014", "3Qw=" },
+  { 2, "\233\217", "m48=" },
+  { 2, "\373\056", "+y4=" },
+  { 2, "\247\232", "p5o=" },
+  { 2, "\107\053", "Rys=" },
+  { 2, "\204\077", "hD8=" },
+  { 2, "\276\211", "vok=" },
+  { 2, "\313\110", "y0g=" },
+  { 2, "\363\376", "8/4=" },
+  { 2, "\251\234", "qZw=" },
+  { 2, "\103\262", "Q7I=" },
+  { 2, "\142\312", "Yso=" },
+  { 2, "\067\211", "N4k=" },
+  { 2, "\220\001", "kAE=" },
+  { 2, "\152\240", "aqA=" },
+  { 2, "\367\061", "9zE=" },
+  { 2, "\133\255", "W60=" },
+  { 2, "\176\035", "fh0=" },
+  { 2, "\032\231", "Gpk=" },
+
+  { 3, "\013\007\144", "Cwdk" },
+  { 3, "\030\112\106", "GEpG" },
+  { 3, "\047\325\046", "J9Um" },
+  { 3, "\310\160\022", "yHAS" },
+  { 3, "\131\100\237", "WUCf" },
+  { 3, "\064\342\134", "NOJc" },
+  { 3, "\010\177\004", "CH8E" },
+  { 3, "\345\147\205", "5WeF" },
+  { 3, "\300\343\360", "wOPw" },
+  { 3, "\061\240\201", "MaCB" },
+  { 3, "\225\333\044", "ldsk" },
+  { 3, "\215\137\352", "jV/q" },
+  { 3, "\371\147\160", "+Wdw" },
+  { 3, "\030\320\051", "GNAp" },
+  { 3, "\044\174\241", "JHyh" },
+  { 3, "\260\127\037", "sFcf" },
+  { 3, "\111\045\033", "SSUb" },
+  { 3, "\202\114\107", "gkxH" },
+  { 3, "\057\371\042", "L/ki" },
+  { 3, "\223\247\244", "k6ek" },
+  { 3, "\047\216\144", "J45k" },
+  { 3, "\203\070\327", "gzjX" },
+  { 3, "\247\140\072", "p2A6" },
+  { 3, "\124\115\116", "VE1O" },
+  { 3, "\157\162\050", "b3Io" },
+  { 3, "\357\223\004", "75ME" },
+  { 3, "\052\117\156", "Kk9u" },
+  { 3, "\347\154\000", "52wA" },
+  { 3, "\303\012\142", "wwpi" },
+  { 3, "\060\035\362", "MB3y" },
+  { 3, "\130\226\361", "WJbx" },
+  { 3, "\173\013\071", "ews5" },
+  { 3, "\336\004\027", "3gQX" },
+  { 3, "\357\366\234", "7/ac" },
+  { 3, "\353\304\111", "68RJ" },
+  { 3, "\024\264\131", "FLRZ" },
+  { 3, "\075\114\251", "PUyp" },
+  { 3, "\315\031\225", "zRmV" },
+  { 3, "\154\201\276", "bIG+" },
+  { 3, "\200\066\072", "gDY6" },
+  { 3, "\142\350\267", "Yui3" },
+  { 3, "\033\000\166", "GwB2" },
+  { 3, "\210\055\077", "iC0/" },
+  { 3, "\341\037\124", "4R9U" },
+  { 3, "\161\103\152", "cUNq" },
+  { 3, "\270\142\131", "uGJZ" },
+  { 3, "\337\076\074", "3z48" },
+  { 3, "\375\106\362", "/Uby" },
+  { 3, "\227\301\127", "l8FX" },
+  { 3, "\340\002\234", "4AKc" },
+  { 3, "\121\064\033", "UTQb" },
+  { 3, "\157\134\143", "b1xj" },
+  { 3, "\247\055\327", "py3X" },
+  { 3, "\340\142\005", "4GIF" },
+  { 3, "\060\260\143", "MLBj" },
+  { 3, "\075\203\170", "PYN4" },
+  { 3, "\143\160\016", "Y3AO" },
+  { 3, "\313\013\063", "ywsz" },
+  { 3, "\174\236\135", "fJ5d" },
+  { 3, "\103\047\026", "QycW" },
+  { 3, "\365\005\343", "9QXj" },
+  { 3, "\271\160\223", "uXCT" },
+  { 3, "\362\255\172", "8q16" },
+  { 3, "\113\012\015", "SwoN" },
+
+  // various lengths, generated by this python script:
+  //
+  // from string import lowercase as lc
+  // for i in range(27):
+  //   print '{ %2d, "%s",%s "%s" },' % (i, lc[:i], ' ' * (26-i),
+  //                                     lc[:i].encode('base64').strip())
+
+  {  0, "abcdefghijklmnopqrstuvwxyz", "" },
+  {  1, "abcdefghijklmnopqrstuvwxyz", "YQ==" },
+  {  2, "abcdefghijklmnopqrstuvwxyz", "YWI=" },
+  {  3, "abcdefghijklmnopqrstuvwxyz", "YWJj" },
+  {  4, "abcdefghijklmnopqrstuvwxyz", "YWJjZA==" },
+  {  5, "abcdefghijklmnopqrstuvwxyz", "YWJjZGU=" },
+  {  6, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVm" },
+  {  7, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZw==" },
+  {  8, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2g=" },
+  {  9, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hp" },
+  { 10, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpag==" },
+  { 11, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpams=" },
+  { 12, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamts" },
+  { 13, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbQ==" },
+  { 14, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW4=" },
+  { 15, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5v" },
+  { 16, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcA==" },
+  { 17, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHE=" },
+  { 18, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFy" },
+  { 19, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFycw==" },
+  { 20, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3Q=" },
+  { 21, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1" },
+  { 22, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dg==" },
+  { 23, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnc=" },
+  { 24, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4" },
+  { 25, "abcdefghijklmnopqrstuvwxy",  "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eQ==" },
+  { 26, "abcdefghijklmnopqrstuvwxyz", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=" },
+};
+#if 0
+static struct {
+  const char* plaintext;
+  const char* cyphertext;
+} base64_strings[] = {
+
+  // The first few Google quotes
+  // Cyphertext created with "uuencode - GNU sharutils 4.2.1"
+  {
+    "Everyone!  We're teetering on the brink of disaster."
+    " - Sergey Brin, 6/24/99, regarding the company's state "
+    "after the unleashing of Netscape/Google search",
+
+    "RXZlcnlvbmUhICBXZSdyZSB0ZWV0ZXJpbmcgb24gdGhlIGJyaW5rIG9mIGRp"
+    "c2FzdGVyLiAtIFNlcmdleSBCcmluLCA2LzI0Lzk5LCByZWdhcmRpbmcgdGhl"
+    "IGNvbXBhbnkncyBzdGF0ZSBhZnRlciB0aGUgdW5sZWFzaGluZyBvZiBOZXRz"
+    "Y2FwZS9Hb29nbGUgc2VhcmNo" },
+
+  {
+    "I'm not sure why we're still alive, but we seem to be."
+    " - Larry Page, 6/24/99, while hiding in the kitchenette "
+    "during the Netscape traffic overflow",
+
+    "SSdtIG5vdCBzdXJlIHdoeSB3ZSdyZSBzdGlsbCBhbGl2ZSwgYnV0IHdlIHNl"
+    "ZW0gdG8gYmUuIC0gTGFycnkgUGFnZSwgNi8yNC85OSwgd2hpbGUgaGlkaW5n"
+    "IGluIHRoZSBraXRjaGVuZXR0ZSBkdXJpbmcgdGhlIE5ldHNjYXBlIHRyYWZm"
+    "aWMgb3ZlcmZsb3c" },
+
+  {
+    "I think kids want porn."
+    " - Sergey Brin, 6/99, on why Google shouldn't prioritize a "
+    "filtered search for children and families",
+
+    "SSB0aGluayBraWRzIHdhbnQgcG9ybi4gLSBTZXJnZXkgQnJpbiwgNi85OSwg"
+    "b24gd2h5IEdvb2dsZSBzaG91bGRuJ3QgcHJpb3JpdGl6ZSBhIGZpbHRlcmVk"
+    "IHNlYXJjaCBmb3IgY2hpbGRyZW4gYW5kIGZhbWlsaWVz" },
+};
+#endif
+// Compare bytes 0..len-1 of x and y.  If not equal, abort with verbose error
+// message showing position and numeric value that differed.
+// Handles embedded nulls just like any other byte.
+// Only added because string.compare() in gcc-3.3.3 seems to misbehave with
+// embedded nulls.
+// TODO: switch back to string.compare() if/when gcc is fixed
+#define EXPECT_EQ_ARRAY(len, x, y, msg)                      \
+  for (size_t j = 0; j < len; ++j) {                           \
+    if (x[j] != y[j]) {                                     \
+        LOG(LS_ERROR) << "" # x << " != " # y                  \
+                   << " byte " << j << " msg: " << msg;     \
+      }                                                     \
+    }
+
+size_t Base64Escape(const unsigned char *src, size_t szsrc, char *dest,
+                    size_t szdest) {
+  std::string escaped;
+  Base64::EncodeFromArray((const char *)src, szsrc, &escaped);
+  memcpy(dest, escaped.data(), min(escaped.size(), szdest));
+  return escaped.size();
+}
+
+size_t Base64Unescape(const char *src, size_t szsrc, char *dest,
+                      size_t szdest) {
+  std::string unescaped;
+  EXPECT_TRUE(Base64::DecodeFromArray(src, szsrc, Base64::DO_LAX, &unescaped,
+                                      NULL));
+  memcpy(dest, unescaped.data(), min(unescaped.size(), szdest));
+  return unescaped.size();
+}
+
+size_t Base64Unescape(const char *src, size_t szsrc, string *s) {
+  EXPECT_TRUE(Base64::DecodeFromArray(src, szsrc, Base64::DO_LAX, s, NULL));
+  return s->size();
+}
+
+TEST(Base64, EncodeDecodeBattery) {
+  LOG(LS_VERBOSE) << "Testing base-64";
+
+  size_t i;
+
+  // Check the short strings; this tests the math (and boundaries)
+  for( i = 0; i < sizeof(base64_tests) / sizeof(base64_tests[0]); ++i ) {
+    char encode_buffer[100];
+    size_t encode_length;
+    char decode_buffer[100];
+    size_t decode_length;
+    size_t cypher_length;
+
+    LOG(LS_VERBOSE) << "B64: " << base64_tests[i].cyphertext;
+
+    const unsigned char* unsigned_plaintext =
+      reinterpret_cast<const unsigned char*>(base64_tests[i].plaintext);
+
+    cypher_length = strlen(base64_tests[i].cyphertext);
+
+    // The basic escape function:
+    memset(encode_buffer, 0, sizeof(encode_buffer));
+    encode_length = Base64Escape(unsigned_plaintext,
+                                 base64_tests[i].plain_length,
+                                 encode_buffer,
+                                 sizeof(encode_buffer));
+    //    Is it of the expected length?
+    EXPECT_EQ(encode_length, cypher_length);
+
+    //    Is it the expected encoded value?
+    EXPECT_STREQ(encode_buffer, base64_tests[i].cyphertext);
+
+    // If we encode it into a buffer of exactly the right length...
+    memset(encode_buffer, 0, sizeof(encode_buffer));
+    encode_length = Base64Escape(unsigned_plaintext,
+                                 base64_tests[i].plain_length,
+                                 encode_buffer,
+                                 cypher_length);
+    //    Is it still of the expected length?
+    EXPECT_EQ(encode_length, cypher_length);
+
+    //    And is the value still correct?  (i.e., not losing the last byte)
+    EXPECT_STREQ(encode_buffer, base64_tests[i].cyphertext);
+
+    // If we decode it back:
+    memset(decode_buffer, 0, sizeof(decode_buffer));
+    decode_length = Base64Unescape(encode_buffer,
+                                   cypher_length,
+                                   decode_buffer,
+                                   sizeof(decode_buffer));
+
+    //    Is it of the expected length?
+    EXPECT_EQ(decode_length, base64_tests[i].plain_length);
+
+    //    Is it the expected decoded value?
+    EXPECT_EQ(0,  memcmp(decode_buffer, base64_tests[i].plaintext, decode_length));
+
+    // Our decoder treats the padding '=' characters at the end as
+    // optional.  If encode_buffer has any, run some additional
+    // tests that fiddle with them.
+    char* first_equals = strchr(encode_buffer, '=');
+    if (first_equals) {
+      // How many equals signs does the string start with?
+      int equals = (*(first_equals+1) == '=') ? 2 : 1;
+
+      // Try chopping off the equals sign(s) entirely.  The decoder
+      // should still be okay with this.
+      string decoded2("this junk should also be ignored");
+      *first_equals = '\0';
+      EXPECT_NE(0U, Base64Unescape(encode_buffer, first_equals-encode_buffer,
+                           &decoded2));
+      EXPECT_EQ(decoded2.size(), base64_tests[i].plain_length);
+      EXPECT_EQ_ARRAY(decoded2.size(), decoded2.data(), base64_tests[i].plaintext, i);
+
+      int len;
+
+      // try putting some extra stuff after the equals signs, or in between them
+      if (equals == 2) {
+        sprintfn(first_equals, 6, " = = ");
+        len = first_equals - encode_buffer + 5;
+      } else {
+        sprintfn(first_equals, 6, " = ");
+        len = first_equals - encode_buffer + 3;
+      }
+      decoded2.assign("this junk should be ignored");
+      EXPECT_NE(0U, Base64Unescape(encode_buffer, len, &decoded2));
+      EXPECT_EQ(decoded2.size(), base64_tests[i].plain_length);
+      EXPECT_EQ_ARRAY(decoded2.size(), decoded2, base64_tests[i].plaintext, i);
+    }
+  }
+}
+
+// here's a weird case: a giant base64 encoded stream which broke our base64
+// decoding.  Let's test it explicitly.
+const char SpecificTest[] =
+  "/9j/4AAQSkZJRgABAgEASABIAAD/4Q0HRXhpZgAATU0AKgAAAAgADAEOAAIAAAAgAAAAngEPAAI\n"
+  "AAAAFAAAAvgEQAAIAAAAJAAAAwwESAAMAAAABAAEAAAEaAAUAAAABAAAAzAEbAAUAAAABAAAA1A\n"
+  "EoAAMAAAABAAIAAAExAAIAAAAUAAAA3AEyAAIAAAAUAAAA8AE8AAIAAAAQAAABBAITAAMAAAABA\n"
+  "AIAAIdpAAQAAAABAAABFAAAAsQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAFNPTlkA\n"
+  "RFNDLVAyMDAAAAAASAAAAAEAAABIAAAAAUFkb2JlIFBob3Rvc2hvcCA3LjAAMjAwNzowMTozMCA\n"
+  "yMzoxMDowNABNYWMgT1MgWCAxMC40LjgAAByCmgAFAAAAAQAAAmqCnQAFAAAAAQAAAnKIIgADAA\n"
+  "AAAQACAACIJwADAAAAAQBkAACQAAAHAAAABDAyMjCQAwACAAAAFAAAAnqQBAACAAAAFAAAAo6RA\n"
+  "QAHAAAABAECAwCRAgAFAAAAAQAAAqKSBAAKAAAAAQAAAqqSBQAFAAAAAQAAArKSBwADAAAAAQAF\n"
+  "AACSCAADAAAAAQAAAACSCQADAAAAAQAPAACSCgAFAAAAAQAAArqgAAAHAAAABDAxMDCgAQADAAA\n"
+  "AAf//AACgAgAEAAAAAQAAAGSgAwAEAAAAAQAAAGSjAAAHAAAAAQMAAACjAQAHAAAAAQEAAACkAQ\n"
+  "ADAAAAAQAAAACkAgADAAAAAQAAAACkAwADAAAAAQAAAACkBgADAAAAAQAAAACkCAADAAAAAQAAA\n"
+  "ACkCQADAAAAAQAAAACkCgADAAAAAQAAAAAAAAAAAAAACgAAAZAAAAAcAAAACjIwMDc6MDE6MjAg\n"
+  "MjM6MDU6NTIAMjAwNzowMToyMCAyMzowNTo1MgAAAAAIAAAAAQAAAAAAAAAKAAAAMAAAABAAAAB\n"
+  "PAAAACgAAAAYBAwADAAAAAQAGAAABGgAFAAAAAQAAAxIBGwAFAAAAAQAAAxoBKAADAAAAAQACAA\n"
+  "ACAQAEAAAAAQAAAyICAgAEAAAAAQAACd0AAAAAAAAASAAAAAEAAABIAAAAAf/Y/+AAEEpGSUYAA\n"
+  "QIBAEgASAAA/+0ADEFkb2JlX0NNAAL/7gAOQWRvYmUAZIAAAAAB/9sAhAAMCAgICQgMCQkMEQsK\n"
+  "CxEVDwwMDxUYExMVExMYEQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQ0LCw0\n"
+  "ODRAODhAUDg4OFBQODg4OFBEMDAwMDBERDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA\n"
+  "wMDAz/wAARCABkAGQDASIAAhEBAxEB/90ABAAH/8QBPwAAAQUBAQEBAQEAAAAAAAAAAwABAgQFB\n"
+  "gcICQoLAQABBQEBAQEBAQAAAAAAAAABAAIDBAUGBwgJCgsQAAEEAQMCBAIFBwYIBQMMMwEAAhED\n"
+  "BCESMQVBUWETInGBMgYUkaGxQiMkFVLBYjM0coLRQwclklPw4fFjczUWorKDJkSTVGRFwqN0Nhf\n"
+  "SVeJl8rOEw9N14/NGJ5SkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2N0dXZ3eHl6e3x9fn9xEAAg\n"
+  "IBAgQEAwQFBgcHBgU1AQACEQMhMRIEQVFhcSITBTKBkRShsUIjwVLR8DMkYuFygpJDUxVjczTxJ\n"
+  "QYWorKDByY1wtJEk1SjF2RFVTZ0ZeLys4TD03Xj80aUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm\n"
+  "9ic3R1dnd4eXp7fH/9oADAMBAAIRAxEAPwDy7bKNTUXNLz9EaJPDWMjxH4ozhtpYwaACT8ShaaW\n"
+  "bW0uEc9/JFfjj0Q4Hk/PRDxwX7y47W9z/AN9Cv4+O3ILK2DcRqT2CaSvEbcl1Jbz37KG1dBldLo\n"
+  "qaS4l9xGjG9v6yoDAdYIaIjUk+AREgo4y5sapirb8Yl0NHHdKvBNm4yA1o5Pc+SPEFvCWqB3HZF\n"
+  "Hj2SbWQ/afGFP0bHP8ATY0uc4w1o1JPkkimGiS2KvqlnmBkOZQTyydzgPMM9v8A0lp4v1Nx9gF1\n"
+  "tpdqJaGtH/S3I0i3lISXW/8AMqnd/O2bfg2eUkqVYf/Q8zuncO4Bj7lZ+n7f5Mj5KsJcY8NUZ4d\n"
+  "uEDVo1HkeU0rg3Om4H2rabCWUN7DQuK1n5FWKW4uCwG92gDRJBS6exhxmMboQI+Cv4WFTQ42Bs2\n"
+  "fvnkkqEmy2YxoMMbpVzaz6jt+RbpHZs8lzkHqrasKkYOKP0jgDfZ4N/wDM1tNrcWfSPmRyq9uNV\n"
+  "DnFg2s97i7UkjxKVrq0eVz3spZsja+ASDzwsh9jnOk/JFzb3XZD3v1c4yT8UACTCniKDUnKz5Nj\n"
+  "G33XV1DV73BrT8dF23SejV4zg9g33cOsPb+SxVvqv9ViwNy8vS0iWs/daf8A0Y5dpTi1sADGxCR\n"
+  "K1o0YBEmInlXWYbDBcDLdPJXa8f71Yrx2jnUoAqLnfZK5hJaW2vdwEk5a/wD/0fN6Ia/e76IiVf\n"
+  "xavUL7CPpnT4LNbYXAVjuQt/AqDmNYO/Kjnoy4hr5J8SwMhrRMaeSvbsxrfUazcOw4UX0Cisem2\n"
+  "SBoD4+Kz8nC6llbSLCRrubJA8kwUWbUDa29X1PMa7aQWjuDC0MXMdbDbhI7eazBiUfZ6GOYRe1s\n"
+  "WvGgJ8Vbw2+m4Bx9s6JpNHuuGo1FF53r/SHYua61gLse0lzXeBP5rkvqx0o5vVWz7WY49QkiQSP\n"
+  "oN/tLoevW/ogxv0HA7tJ0AnhT+pdDGYVl/wCdcTPkGn2NU0JWNWvlgAbHV6fEqdu2gR/r2WlWwt\n"
+  "AA5VXAEsLXTqJafArQY5rRr9LiPBJiZsZCI1pJjxCi0j4oncSICSkWwzwkjeaSch//0vO7sP7Lm\n"
+  "enO9ogtd5FbPT3Q5pCpZVc4ld3Lmn3O8j9EI2BYdunKjOobMQIyI+rusc2wx4d0eutwGnHh/uQc\n"
+  "Ha7ladj6mVANGvcqOgz0Go7HJ12/GEHcwvB/dPY6ImbbaMaASGuIBjkN7qofs9Ubg9g7OI9p/t/\n"
+  "RTSmhTHr0v6eSz6UgCPP2/wAVu9Ex2V49dVY2iACB4BZeVXQ/AJ3gzGnnOi2+kACpru8flUsNmt\n"
+  "zHRf6xfWCnoeAfTh2ZaQKazx/Ke7+QxcKz61fWA2uuObaC4zGhaPJrXBL64ZFmR124O09ENraPK\n"
+  "N3/AH5GqxIrZVUyp2K2vfdkENsDnxuex9m4Ox9n82xSgNd9D+p/XR1npgseR9ppOy4Dx/NfH/CL\n"
+  "oQJGunmvMv8AFq3KHVcq3HkYQbD2nuSf0I/rMavSg6TLjLigQhJ7Z58v9QkmlsTOqSCn/9PzL7R\n"
+  "d6Qq3n0wZ2zotXpT9xLfFYvkr/S7jXeB8E0jRkhKpC3q8LcJ/kmCrTnkuAPCq4do9Q/ytVbuAeY\n"
+  "Gg5lQybQK+82GBqEQUA1kOHPYf3LLsoyN36G5w8iUfHxepbXE2l0cApALgLHzBq9UxhTXU5hMC1\n"
+  "ktnSCup6S4Ctk+C5XqVGcaHPfuiuHkeTTuWz0+9zaKiH6CC0/yXBSQ2a/MxojV57634rq+v2PLY\n"
+  "be1r2nsYG13/AFKxbfCBMcr0brGAzrGEwCG31ncx0SfBzf7S4+zoHUWWsJq3hz9oLfcBH77R9H+\n"
+  "0pA13u/qPgDp/Q6ri39JlfpXkDx+h/msWn1L6wdO6bSbcrIbU2Q0xLnSe21kuVejJspbVS5+4bd\n"
+  "ocBAkD/orG+tP1ar67Wy7GtZTm1SCXfRsb+a18fRe38x6SG3/44H1Z3f0y2I+l6DoSXD/8xPrDs\n"
+  "3enVu3bdnqN3R+//USSVo//1PLohhce+gRWS0Nsby3lRgFkKxQyW7SgUh3em5Tbq2uB9wWw1wey\n"
+  "J1XGV2XYdm5k7e4WzidXY9oMwo5RZ4T6Hd1ixwfp96PWbAJBVTHzK7O6Ky5oJB1HZMqmUEFlkGy\n"
+  "xpa4zI1Hkq31dy7bMN9BAc3HeWAnnbyxEycmuup1jiAGglZ31PyrmZ9tQg1WtNj54EHR3/S2qTH\n"
+  "1Yc5GgD1FFtzPdWGkd2AyflogZmRmsz6PSrbXbdo+txOrP337f3fzVo15DK2uyrTtqpBOnBKx6b\n"
+  "7MjJsz7tHWOAYP3WD6LU6cqGjFCNl1MmvLcxv6YtDTLSAqP27LrdtYHXFnJZI+Tp3MWg68OpDPv\n"
+  "UMUM2lkQBoouKQ6swjE9Nml+1sz1PW+z6xt27zuj+skrX2ZvqR5z8kkuOfdPt43/1fMm/grFG6f\n"
+  "Lss9JA7JG7tnZs/SfJUrfS3foJ9TvHCopJsV8nWx/t24bJn8Fo/5TjWJXMJIS+i+G36TsZ/7Q9P\n"
+  "8ATfzfeOFofVSZv2/zvt+O3X/v65dJPjt/BiyfN1/wn0zre79nVej/ADG8ep4x2/6Srjd6TdviF\n"
+  "52ko8m6/Ht9X1KnftEo+POwxzK8mSTF46vrH6T1/OEl5Okkl//Z/+0uHFBob3Rvc2hvcCAzLjAA\n"
+  "OEJJTQQEAAAAAAArHAIAAAIAAhwCeAAfICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAA\n"
+  "4QklNBCUAAAAAABD7Caa9B0wqNp2P4sxXqayFOEJJTQPqAAAAAB2wPD94bWwgdmVyc2lvbj0iMS\n"
+  "4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUgQ\n"
+  "29tcHV0ZXIvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Q\n"
+  "cm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk\n"
+  "+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1Ib3Jpem9udGFsUmVzPC9rZXk+Cgk8ZGljdD\n"
+  "4KCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuY3JlYXRvcjwva2V5PgoJCTxzdHJpbmc+Y\n"
+  "29tLmFwcGxlLnByaW50aW5nbWFuYWdlcjwvc3RyaW5nPgoJCTxrZXk+Y29tLmFwcGxlLnByaW50\n"
+  "LnRpY2tldC5pdGVtQXJyYXk8L2tleT4KCQk8YXJyYXk+CgkJCTxkaWN0PgoJCQkJPGtleT5jb20\n"
+  "uYXBwbGUucHJpbnQuUGFnZUZvcm1hdC5QTUhvcml6b250YWxSZXM8L2tleT4KCQkJCTxyZWFsPj\n"
+  "cyPC9yZWFsPgoJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNsaWVudDwva2V5PgoJC\n"
+  "QkJPHN0cmluZz5jb20uYXBwbGUucHJpbnRpbmdtYW5hZ2VyPC9zdHJpbmc+CgkJCQk8a2V5PmNv\n"
+  "bS5hcHBsZS5wcmludC50aWNrZXQubW9kRGF0ZTwva2V5PgoJCQkJPGRhdGU+MjAwNy0wMS0zMFQ\n"
+  "yMjowODo0MVo8L2RhdGU+CgkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuc3RhdGVGbG\n"
+  "FnPC9rZXk+CgkJCQk8aW50ZWdlcj4wPC9pbnRlZ2VyPgoJCQk8L2RpY3Q+CgkJPC9hcnJheT4KC\n"
+  "TwvZGljdD4KCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1PcmllbnRhdGlvbjwv\n"
+  "a2V5PgoJPGRpY3Q+CgkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNyZWF0b3I8L2tleT4\n"
+  "KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZXI8L3N0cmluZz4KCQk8a2V5PmNvbS\n"
+  "5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJPGFycmF5PgoJCQk8ZGljdD4KC\n"
+  "QkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1PcmllbnRhdGlvbjwva2V5PgoJ\n"
+  "CQkJPGludGVnZXI+MTwvaW50ZWdlcj4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5\n"
+  "jbGllbnQ8L2tleT4KCQkJCTxzdHJpbmc+Y29tLmFwcGxlLnByaW50aW5nbWFuYWdlcjwvc3RyaW\n"
+  "5nPgoJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0Lm1vZERhdGU8L2tleT4KCQkJCTxkY\n"
+  "XRlPjIwMDctMDEtMzBUMjI6MDg6NDFaPC9kYXRlPgoJCQkJPGtleT5jb20uYXBwbGUucHJpbnQu\n"
+  "dGlja2V0LnN0YXRlRmxhZzwva2V5PgoJCQkJPGludGVnZXI+MDwvaW50ZWdlcj4KCQkJPC9kaWN\n"
+  "0PgoJCTwvYXJyYXk+Cgk8L2RpY3Q+Cgk8a2V5PmNvbS5hcHBsZS5wcmludC5QYWdlRm9ybWF0Ll\n"
+  "BNU2NhbGluZzwva2V5PgoJPGRpY3Q+CgkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNyZ\n"
+  "WF0b3I8L2tleT4KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZXI8L3N0cmluZz4K\n"
+  "CQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJPGFycmF5Pgo\n"
+  "JCQk8ZGljdD4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1TY2FsaW5nPC\n"
+  "9rZXk+CgkJCQk8cmVhbD4xPC9yZWFsPgoJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0L\n"
+  "mNsaWVudDwva2V5PgoJCQkJPHN0cmluZz5jb20uYXBwbGUucHJpbnRpbmdtYW5hZ2VyPC9zdHJp\n"
+  "bmc+CgkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQubW9kRGF0ZTwva2V5PgoJCQkJPGR\n"
+  "hdGU+MjAwNy0wMS0zMFQyMjowODo0MVo8L2RhdGU+CgkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC\n"
+  "50aWNrZXQuc3RhdGVGbGFnPC9rZXk+CgkJCQk8aW50ZWdlcj4wPC9pbnRlZ2VyPgoJCQk8L2RpY\n"
+  "3Q+CgkJPC9hcnJheT4KCTwvZGljdD4KCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQu\n"
+  "UE1WZXJ0aWNhbFJlczwva2V5PgoJPGRpY3Q+CgkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V\n"
+  "0LmNyZWF0b3I8L2tleT4KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZXI8L3N0cm\n"
+  "luZz4KCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJPGFyc\n"
+  "mF5PgoJCQk8ZGljdD4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYXQuUE1WZXJ0\n"
+  "aWNhbFJlczwva2V5PgoJCQkJPHJlYWw+NzI8L3JlYWw+CgkJCQk8a2V5PmNvbS5hcHBsZS5wcml\n"
+  "udC50aWNrZXQuY2xpZW50PC9rZXk+CgkJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbm\n"
+  "FnZXI8L3N0cmluZz4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5tb2REYXRlPC9rZ\n"
+  "Xk+CgkJCQk8ZGF0ZT4yMDA3LTAxLTMwVDIyOjA4OjQxWjwvZGF0ZT4KCQkJCTxrZXk+Y29tLmFw\n"
+  "cGxlLnByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCTxpbnRlZ2VyPjA8L2ludGVnZXI\n"
+  "+CgkJCTwvZGljdD4KCQk8L2FycmF5PgoJPC9kaWN0PgoJPGtleT5jb20uYXBwbGUucHJpbnQuUG\n"
+  "FnZUZvcm1hdC5QTVZlcnRpY2FsU2NhbGluZzwva2V5PgoJPGRpY3Q+CgkJPGtleT5jb20uYXBwb\n"
+  "GUucHJpbnQudGlja2V0LmNyZWF0b3I8L2tleT4KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGlu\n"
+  "Z21hbmFnZXI8L3N0cmluZz4KCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF\n"
+  "5PC9rZXk+CgkJPGFycmF5PgoJCQk8ZGljdD4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2\n"
+  "VGb3JtYXQuUE1WZXJ0aWNhbFNjYWxpbmc8L2tleT4KCQkJCTxyZWFsPjE8L3JlYWw+CgkJCQk8a\n"
+  "2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuY2xpZW50PC9rZXk+CgkJCQk8c3RyaW5nPmNvbS5h\n"
+  "cHBsZS5wcmludGluZ21hbmFnZXI8L3N0cmluZz4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnR\n"
+  "pY2tldC5tb2REYXRlPC9rZXk+CgkJCQk8ZGF0ZT4yMDA3LTAxLTMwVDIyOjA4OjQxWjwvZGF0ZT\n"
+  "4KCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCTxpb\n"
+  "nRlZ2VyPjA8L2ludGVnZXI+CgkJCTwvZGljdD4KCQk8L2FycmF5PgoJPC9kaWN0PgoJPGtleT5j\n"
+  "b20uYXBwbGUucHJpbnQuc3ViVGlja2V0LnBhcGVyX2luZm9fdGlja2V0PC9rZXk+Cgk8ZGljdD4\n"
+  "KCQk8a2V5PmNvbS5hcHBsZS5wcmludC5QYWdlRm9ybWF0LlBNQWRqdXN0ZWRQYWdlUmVjdDwva2\n"
+  "V5PgoJCTxkaWN0PgoJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuY3JlYXRvcjwva2V5P\n"
+  "goJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZXI8L3N0cmluZz4KCQkJPGtleT5j\n"
+  "b20uYXBwbGUucHJpbnQudGlja2V0Lml0ZW1BcnJheTwva2V5PgoJCQk8YXJyYXk+CgkJCQk8ZGl\n"
+  "jdD4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC5QYWdlRm9ybWF0LlBNQWRqdXN0ZWRQYWdlUm\n"
+  "VjdDwva2V5PgoJCQkJCTxhcnJheT4KCQkJCQkJPHJlYWw+MC4wPC9yZWFsPgoJCQkJCQk8cmVhb\n"
+  "D4wLjA8L3JlYWw+CgkJCQkJCTxyZWFsPjczNDwvcmVhbD4KCQkJCQkJPHJlYWw+NTc2PC9yZWFs\n"
+  "PgoJCQkJCTwvYXJyYXk+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNsaWVudDw\n"
+  "va2V5PgoJCQkJCTxzdHJpbmc+Y29tLmFwcGxlLnByaW50aW5nbWFuYWdlcjwvc3RyaW5nPgoJCQ\n"
+  "kJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5tb2REYXRlPC9rZXk+CgkJCQkJPGRhdGU+M\n"
+  "jAwNy0wMS0zMFQyMjowODo0MVo8L2RhdGU+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlj\n"
+  "a2V0LnN0YXRlRmxhZzwva2V5PgoJCQkJCTxpbnRlZ2VyPjA8L2ludGVnZXI+CgkJCQk8L2RpY3Q\n"
+  "+CgkJCTwvYXJyYXk+CgkJPC9kaWN0PgoJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhZ2VGb3JtYX\n"
+  "QuUE1BZGp1c3RlZFBhcGVyUmVjdDwva2V5PgoJCTxkaWN0PgoJCQk8a2V5PmNvbS5hcHBsZS5wc\n"
+  "mludC50aWNrZXQuY3JlYXRvcjwva2V5PgoJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21h\n"
+  "bmFnZXI8L3N0cmluZz4KCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0Lml0ZW1BcnJheTw\n"
+  "va2V5PgoJCQk8YXJyYXk+CgkJCQk8ZGljdD4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC5QYW\n"
+  "dlRm9ybWF0LlBNQWRqdXN0ZWRQYXBlclJlY3Q8L2tleT4KCQkJCQk8YXJyYXk+CgkJCQkJCTxyZ\n"
+  "WFsPi0xODwvcmVhbD4KCQkJCQkJPHJlYWw+LTE4PC9yZWFsPgoJCQkJCQk8cmVhbD43NzQ8L3Jl\n"
+  "YWw+CgkJCQkJCTxyZWFsPjU5NDwvcmVhbD4KCQkJCQk8L2FycmF5PgoJCQkJCTxrZXk+Y29tLmF\n"
+  "wcGxlLnByaW50LnRpY2tldC5jbGllbnQ8L2tleT4KCQkJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcm\n"
+  "ludGluZ21hbmFnZXI8L3N0cmluZz4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQub\n"
+  "W9kRGF0ZTwva2V5PgoJCQkJCTxkYXRlPjIwMDctMDEtMzBUMjI6MDg6NDFaPC9kYXRlPgoJCQkJ\n"
+  "CTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCQk8aW50ZWd\n"
+  "lcj4wPC9pbnRlZ2VyPgoJCQkJPC9kaWN0PgoJCQk8L2FycmF5PgoJCTwvZGljdD4KCQk8a2V5Pm\n"
+  "NvbS5hcHBsZS5wcmludC5QYXBlckluZm8uUE1QYXBlck5hbWU8L2tleT4KCQk8ZGljdD4KCQkJP\n"
+  "GtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNyZWF0b3I8L2tleT4KCQkJPHN0cmluZz5jb20u\n"
+  "YXBwbGUucHJpbnQucG0uUG9zdFNjcmlwdDwvc3RyaW5nPgoJCQk8a2V5PmNvbS5hcHBsZS5wcml\n"
+  "udC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJCTxhcnJheT4KCQkJCTxkaWN0PgoJCQkJCTxrZX\n"
+  "k+Y29tLmFwcGxlLnByaW50LlBhcGVySW5mby5QTVBhcGVyTmFtZTwva2V5PgoJCQkJCTxzdHJpb\n"
+  "mc+bmEtbGV0dGVyPC9zdHJpbmc+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNs\n"
+  "aWVudDwva2V5PgoJCQkJCTxzdHJpbmc+Y29tLmFwcGxlLnByaW50LnBtLlBvc3RTY3JpcHQ8L3N\n"
+  "0cmluZz4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQubW9kRGF0ZTwva2V5PgoJCQ\n"
+  "kJCTxkYXRlPjIwMDMtMDctMDFUMTc6NDk6MzZaPC9kYXRlPgoJCQkJCTxrZXk+Y29tLmFwcGxlL\n"
+  "nByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCQk8aW50ZWdlcj4xPC9pbnRlZ2VyPgoJ\n"
+  "CQkJPC9kaWN0PgoJCQk8L2FycmF5PgoJCTwvZGljdD4KCQk8a2V5PmNvbS5hcHBsZS5wcmludC5\n"
+  "QYXBlckluZm8uUE1VbmFkanVzdGVkUGFnZVJlY3Q8L2tleT4KCQk8ZGljdD4KCQkJPGtleT5jb2\n"
+  "0uYXBwbGUucHJpbnQudGlja2V0LmNyZWF0b3I8L2tleT4KCQkJPHN0cmluZz5jb20uYXBwbGUuc\n"
+  "HJpbnQucG0uUG9zdFNjcmlwdDwvc3RyaW5nPgoJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNr\n"
+  "ZXQuaXRlbUFycmF5PC9rZXk+CgkJCTxhcnJheT4KCQkJCTxkaWN0PgoJCQkJCTxrZXk+Y29tLmF\n"
+  "wcGxlLnByaW50LlBhcGVySW5mby5QTVVuYWRqdXN0ZWRQYWdlUmVjdDwva2V5PgoJCQkJCTxhcn\n"
+  "JheT4KCQkJCQkJPHJlYWw+MC4wPC9yZWFsPgoJCQkJCQk8cmVhbD4wLjA8L3JlYWw+CgkJCQkJC\n"
+  "TxyZWFsPjczNDwvcmVhbD4KCQkJCQkJPHJlYWw+NTc2PC9yZWFsPgoJCQkJCTwvYXJyYXk+CgkJ\n"
+  "CQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNsaWVudDwva2V5PgoJCQkJCTxzdHJpbmc\n"
+  "+Y29tLmFwcGxlLnByaW50aW5nbWFuYWdlcjwvc3RyaW5nPgoJCQkJCTxrZXk+Y29tLmFwcGxlLn\n"
+  "ByaW50LnRpY2tldC5tb2REYXRlPC9rZXk+CgkJCQkJPGRhdGU+MjAwNy0wMS0zMFQyMjowODo0M\n"
+  "Vo8L2RhdGU+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LnN0YXRlRmxhZzwva2V5\n"
+  "PgoJCQkJCTxpbnRlZ2VyPjA8L2ludGVnZXI+CgkJCQk8L2RpY3Q+CgkJCTwvYXJyYXk+CgkJPC9\n"
+  "kaWN0PgoJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhcGVySW5mby5QTVVuYWRqdXN0ZWRQYXBlcl\n"
+  "JlY3Q8L2tleT4KCQk8ZGljdD4KCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V0LmNyZWF0b\n"
+  "3I8L2tleT4KCQkJPHN0cmluZz5jb20uYXBwbGUucHJpbnQucG0uUG9zdFNjcmlwdDwvc3RyaW5n\n"
+  "PgoJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuaXRlbUFycmF5PC9rZXk+CgkJCTxhcnJ\n"
+  "heT4KCQkJCTxkaWN0PgoJCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LlBhcGVySW5mby5QTVVuYW\n"
+  "RqdXN0ZWRQYXBlclJlY3Q8L2tleT4KCQkJCQk8YXJyYXk+CgkJCQkJCTxyZWFsPi0xODwvcmVhb\n"
+  "D4KCQkJCQkJPHJlYWw+LTE4PC9yZWFsPgoJCQkJCQk8cmVhbD43NzQ8L3JlYWw+CgkJCQkJCTxy\n"
+  "ZWFsPjU5NDwvcmVhbD4KCQkJCQk8L2FycmF5PgoJCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnR\n"
+  "pY2tldC5jbGllbnQ8L2tleT4KCQkJCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmludGluZ21hbmFnZX\n"
+  "I8L3N0cmluZz4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQubW9kRGF0ZTwva2V5P\n"
+  "goJCQkJCTxkYXRlPjIwMDctMDEtMzBUMjI6MDg6NDFaPC9kYXRlPgoJCQkJCTxrZXk+Y29tLmFw\n"
+  "cGxlLnByaW50LnRpY2tldC5zdGF0ZUZsYWc8L2tleT4KCQkJCQk8aW50ZWdlcj4wPC9pbnRlZ2V\n"
+  "yPgoJCQkJPC9kaWN0PgoJCQk8L2FycmF5PgoJCTwvZGljdD4KCQk8a2V5PmNvbS5hcHBsZS5wcm\n"
+  "ludC5QYXBlckluZm8ucHBkLlBNUGFwZXJOYW1lPC9rZXk+CgkJPGRpY3Q+CgkJCTxrZXk+Y29tL\n"
+  "mFwcGxlLnByaW50LnRpY2tldC5jcmVhdG9yPC9rZXk+CgkJCTxzdHJpbmc+Y29tLmFwcGxlLnBy\n"
+  "aW50LnBtLlBvc3RTY3JpcHQ8L3N0cmluZz4KCQkJPGtleT5jb20uYXBwbGUucHJpbnQudGlja2V\n"
+  "0Lml0ZW1BcnJheTwva2V5PgoJCQk8YXJyYXk+CgkJCQk8ZGljdD4KCQkJCQk8a2V5PmNvbS5hcH\n"
+  "BsZS5wcmludC5QYXBlckluZm8ucHBkLlBNUGFwZXJOYW1lPC9rZXk+CgkJCQkJPHN0cmluZz5VU\n"
+  "yBMZXR0ZXI8L3N0cmluZz4KCQkJCQk8a2V5PmNvbS5hcHBsZS5wcmludC50aWNrZXQuY2xpZW50\n"
+  "PC9rZXk+CgkJCQkJPHN0cmluZz5jb20uYXBwbGUucHJpbnQucG0uUG9zdFNjcmlwdDwvc3RyaW5\n"
+  "nPgoJCQkJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2tldC5tb2REYXRlPC9rZXk+CgkJCQkJPG\n"
+  "RhdGU+MjAwMy0wNy0wMVQxNzo0OTozNlo8L2RhdGU+CgkJCQkJPGtleT5jb20uYXBwbGUucHJpb\n"
+  "nQudGlja2V0LnN0YXRlRmxhZzwva2V5PgoJCQkJCTxpbnRlZ2VyPjE8L2ludGVnZXI+CgkJCQk8\n"
+  "L2RpY3Q+CgkJCTwvYXJyYXk+CgkJPC9kaWN0PgoJCTxrZXk+Y29tLmFwcGxlLnByaW50LnRpY2t\n"
+  "ldC5BUElWZXJzaW9uPC9rZXk+CgkJPHN0cmluZz4wMC4yMDwvc3RyaW5nPgoJCTxrZXk+Y29tLm\n"
+  "FwcGxlLnByaW50LnRpY2tldC5wcml2YXRlTG9jazwva2V5PgoJCTxmYWxzZS8+CgkJPGtleT5jb\n"
+  "20uYXBwbGUucHJpbnQudGlja2V0LnR5cGU8L2tleT4KCQk8c3RyaW5nPmNvbS5hcHBsZS5wcmlu\n"
+  "dC5QYXBlckluZm9UaWNrZXQ8L3N0cmluZz4KCTwvZGljdD4KCTxrZXk+Y29tLmFwcGxlLnByaW5\n"
+  "0LnRpY2tldC5BUElWZXJzaW9uPC9rZXk+Cgk8c3RyaW5nPjAwLjIwPC9zdHJpbmc+Cgk8a2V5Pm\n"
+  "NvbS5hcHBsZS5wcmludC50aWNrZXQucHJpdmF0ZUxvY2s8L2tleT4KCTxmYWxzZS8+Cgk8a2V5P\n"
+  "mNvbS5hcHBsZS5wcmludC50aWNrZXQudHlwZTwva2V5PgoJPHN0cmluZz5jb20uYXBwbGUucHJp\n"
+  "bnQuUGFnZUZvcm1hdFRpY2tldDwvc3RyaW5nPgo8L2RpY3Q+CjwvcGxpc3Q+CjhCSU0D6QAAAAA\n"
+  "AeAADAAAASABIAAAAAALeAkD/7v/uAwYCUgNnBSgD/AACAAAASABIAAAAAALYAigAAQAAAGQAAA\n"
+  "ABAAMDAwAAAAF//wABAAEAAAAAAAAAAAAAAABoCAAZAZAAAAAAACAAAAAAAAAAAAAAAAAAAAAAA\n"
+  "AAAAAAAAAAAADhCSU0D7QAAAAAAEABIAAAAAQABAEgAAAABAAE4QklNBCYAAAAAAA4AAAAAAAAA\n"
+  "AAAAP4AAADhCSU0EDQAAAAAABAAAAB44QklNBBkAAAAAAAQAAAAeOEJJTQPzAAAAAAAJAAAAAAA\n"
+  "AAAABADhCSU0ECgAAAAAAAQAAOEJJTScQAAAAAAAKAAEAAAAAAAAAAThCSU0D9QAAAAAASAAvZm\n"
+  "YAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAA\n"
+  "QAtAAAABgAAAAAAAThCSU0D+AAAAAAAcAAA/////////////////////////////wPoAAAAAP//\n"
+  "//////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA///\n"
+  "//////////////////////////wPoAAA4QklNBAgAAAAAABAAAAABAAACQAAAAkAAAAAAOEJJTQ\n"
+  "QeAAAAAAAEAAAAADhCSU0EGgAAAAADRQAAAAYAAAAAAAAAAAAAAGQAAABkAAAACABEAFMAQwAwA\n"
+  "DIAMwAyADUAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAGQAAABkAAAAAAAAAAAA\n"
+  "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAEAAAAAAABudWxsAAAAAgAAAAZib3VuZHN\n"
+  "PYmpjAAAAAQAAAAAAAFJjdDEAAAAEAAAAAFRvcCBsb25nAAAAAAAAAABMZWZ0bG9uZwAAAAAAAA\n"
+  "AAQnRvbWxvbmcAAABkAAAAAFJnaHRsb25nAAAAZAAAAAZzbGljZXNWbExzAAAAAU9iamMAAAABA\n"
+  "AAAAAAFc2xpY2UAAAASAAAAB3NsaWNlSURsb25nAAAAAAAAAAdncm91cElEbG9uZwAAAAAAAAAG\n"
+  "b3JpZ2luZW51bQAAAAxFU2xpY2VPcmlnaW4AAAANYXV0b0dlbmVyYXRlZAAAAABUeXBlZW51bQA\n"
+  "AAApFU2xpY2VUeXBlAAAAAEltZyAAAAAGYm91bmRzT2JqYwAAAAEAAAAAAABSY3QxAAAABAAAAA\n"
+  "BUb3AgbG9uZwAAAAAAAAAATGVmdGxvbmcAAAAAAAAAAEJ0b21sb25nAAAAZAAAAABSZ2h0bG9uZ\n"
+  "wAAAGQAAAADdXJsVEVYVAAAAAEAAAAAAABudWxsVEVYVAAAAAEAAAAAAABNc2dlVEVYVAAAAAEA\n"
+  "AAAAAAZhbHRUYWdURVhUAAAAAQAAAAAADmNlbGxUZXh0SXNIVE1MYm9vbAEAAAAIY2VsbFRleHR\n"
+  "URVhUAAAAAQAAAAAACWhvcnpBbGlnbmVudW0AAAAPRVNsaWNlSG9yekFsaWduAAAAB2RlZmF1bH\n"
+  "QAAAAJdmVydEFsaWduZW51bQAAAA9FU2xpY2VWZXJ0QWxpZ24AAAAHZGVmYXVsdAAAAAtiZ0Nvb\n"
+  "G9yVHlwZWVudW0AAAARRVNsaWNlQkdDb2xvclR5cGUAAAAATm9uZQAAAAl0b3BPdXRzZXRsb25n\n"
+  "AAAAAAAAAApsZWZ0T3V0c2V0bG9uZwAAAAAAAAAMYm90dG9tT3V0c2V0bG9uZwAAAAAAAAALcml\n"
+  "naHRPdXRzZXRsb25nAAAAAAA4QklNBBEAAAAAAAEBADhCSU0EFAAAAAAABAAAAAE4QklNBAwAAA\n"
+  "AACfkAAAABAAAAZAAAAGQAAAEsAAB1MAAACd0AGAAB/9j/4AAQSkZJRgABAgEASABIAAD/7QAMQ\n"
+  "WRvYmVfQ00AAv/uAA5BZG9iZQBkgAAAAAH/2wCEAAwICAgJCAwJCQwRCwoLERUPDAwPFRgTExUT\n"
+  "ExgRDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBDQsLDQ4NEA4OEBQODg4UFA4\n"
+  "ODg4UEQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAGQAZA\n"
+  "MBIgACEQEDEQH/3QAEAAf/xAE/AAABBQEBAQEBAQAAAAAAAAADAAECBAUGBwgJCgsBAAEFAQEBA\n"
+  "QEBAAAAAAAAAAEAAgMEBQYHCAkKCxAAAQQBAwIEAgUHBggFAwwzAQACEQMEIRIxBUFRYRMicYEy\n"
+  "BhSRobFCIyQVUsFiMzRygtFDByWSU/Dh8WNzNRaisoMmRJNUZEXCo3Q2F9JV4mXys4TD03Xj80Y\n"
+  "nlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3EQACAgECBAQDBAUGBwcGBT\n"
+  "UBAAIRAyExEgRBUWFxIhMFMoGRFKGxQiPBUtHwMyRi4XKCkkNTFWNzNPElBhaisoMHJjXC0kSTV\n"
+  "KMXZEVVNnRl4vKzhMPTdePzRpSkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2JzdHV2d3h5ent8f/\n"
+  "2gAMAwEAAhEDEQA/APLtso1NRc0vP0Rok8NYyPEfijOG2ljBoAJPxKFppZtbS4Rz38kV+OPRDge\n"
+  "T89EPHBfvLjtb3P8A30K/j47cgsrYNxGpPYJpK8RtyXUlvPfsobV0GV0uippLiX3EaMb2/rKgMB\n"
+  "1ghoiNST4BESCjjLmxqmKtvxiXQ0cd0q8E2bjIDWjk9z5I8QW8JaoHcdkUePZJtZD9p8YU/Rsc/\n"
+  "wBNjS5zjDWjUk+SSKYaJLYq+qWeYGQ5lBPLJ3OA8wz2/wDSWni/U3H2AXW2l2oloa0f9LcjSLeU\n"
+  "hJdb/wAyqd387Zt+DZ5SSpVh/9DzO6dw7gGPuVn6ft/kyPkqwlxjw1Rnh24QNWjUeR5TSuDc6bg\n"
+  "fatpsJZQ3sNC4rWfkVYpbi4LAb3aANEkFLp7GHGYxuhAj4K/hYVNDjYGzZ++eSSoSbLZjGgwxul\n"
+  "XNrPqO35FukdmzyXOQeqtqwqRg4o/SOAN9ng3/AMzW02txZ9I+ZHKr241UOcWDaz3uLtSSPEpWu\n"
+  "rR5XPeylmyNr4BIPPCyH2Oc6T8kXNvddkPe/VzjJPxQAJMKeIoNScrPk2MbfddXUNXvcGtPx0Xb\n"
+  "dJ6NXjOD2Dfdw6w9v5LFW+q/1WLA3Ly9LSJaz91p/wDRjl2lOLWwAMbEJErWjRgESYieVdZhsMF\n"
+  "wMt08ldrx/vVivHaOdSgCoud9krmElpba93ASTlr/AP/R83ohr97voiJV/Fq9QvsI+mdPgs1thc\n"
+  "BWO5C38CoOY1g78qOejLiGvknxLAyGtExp5K9uzGt9RrNw7DhRfQKKx6bZIGgPj4rPycLqWVtIs\n"
+  "JGu5skDyTBRZtQNrb1fU8xrtpBaO4MLQxcx1sNuEjt5rMGJR9noY5hF7Wxa8aAnxVvDb6bgHH2z\n"
+  "omk0e64ajUUXnev9Idi5rrWAux7SXNd4E/muS+rHSjm9VbPtZjj1CSJBI+g3+0uh69b+iDG/QcD\n"
+  "u0nQCeFP6l0MZhWX/AJ1xM+QafY1TQlY1a+WABsdXp8Sp27aBH+vZaVbC0ADlVcASwtdOolp8Ct\n"
+  "BjmtGv0uI8EmJmxkIjWkmPEKLSPiidxIgJKRbDPCSN5pJyH//S87uw/suZ6c72iC13kVs9PdDmk\n"
+  "KllVziV3cuafc7yP0QjYFh26cqM6hsxAjIj6u6xzbDHh3R663AaceH+5BwdruVp2PqZUA0a9yo6\n"
+  "DPQajscnXb8YQdzC8H909joiZttoxoBIa4gGOQ3uqh+z1RuD2Ds4j2n+39FNKaFMevS/p5LPpSA\n"
+  "I8/b/ABW70THZXj11VjaIAIHgFl5VdD8AneDMaec6Lb6QAKmu7x+VSw2a3MdF/rF9YKeh4B9OHZ\n"
+  "lpAprPH8p7v5DFwrPrV9YDa645toLjMaFo8mtcEvrhkWZHXbg7T0Q2to8o3f8AfkarEitlVTKnY\n"
+  "ra992QQ2wOfG57H2bg7H2fzbFKA130P6n9dHWemCx5H2mk7LgPH818f8IuhAka6ea8y/wAWrcod\n"
+  "VyrceRhBsPae5J/Qj+sxq9KDpMuMuKBCEntnny/1CSaWxM6pIKf/0/MvtF3pCrefTBnbOi1elP3\n"
+  "Et8Vi+Sv9LuNd4HwTSNGSEqkLerwtwn+SYKtOeS4A8Krh2j1D/K1Vu4B5gaDmVDJtAr7zYYGoRB\n"
+  "QDWQ4c9h/csuyjI3fobnDyJR8fF6ltcTaXRwCkAuAsfMGr1TGFNdTmEwLWS2dIK6npLgK2T4Lle\n"
+  "pUZxoc9+6K4eR5NO5bPT73NoqIfoILT/JcFJDZr8zGiNXnvrfiur6/Y8tht7WvaexgbXf8AUrFt\n"
+  "8IExyvRusYDOsYTAIbfWdzHRJ8HN/tLj7OgdRZawmreHP2gt9wEfvtH0f7SkDXe7+o+AOn9DquL\n"
+  "f0mV+leQPH6H+axafUvrB07ptJtyshtTZDTEudJ7bWS5V6MmyltVLn7ht2hwECQP+isb60/Vqvr\n"
+  "tbLsa1lObVIJd9Gxv5rXx9F7fzHpIbf/jgfVnd/TLYj6XoOhJcP/zE+sOzd6dW7dt2eo3dH7/9R\n"
+  "JJWj//U8uiGFx76BFZLQ2xvLeVGAWQrFDJbtKBSHd6blNura4H3BbDXB7InVcZXZdh2bmTt7hbO\n"
+  "J1dj2gzCjlFnhPod3WLHB+n3o9ZsAkFVMfMrs7orLmgkHUdkyqZQQWWQbLGlrjMjUeSrfV3Ltsw\n"
+  "30EBzcd5YCedvLETJya66nWOIAaCVnfU/KuZn21CDVa02PngQdHf9LapMfVhzkaAPUUW3M91YaR\n"
+  "3YDJ+WiBmZGazPo9Kttdt2j63E6s/fft/d/NWjXkMra7KtO2qkE6cErHpvsyMmzPu0dY4Bg/dYP\n"
+  "otTpyoaMUI2XUya8tzG/pi0NMtICo/bsut21gdcWclkj5OncxaDrw6kM+9QxQzaWRAGii4pDqzC\n"
+  "MT02aX7WzPU9b7PrG3bvO6P6yStfZm+pHnPySS4590+3jf/V8yb+CsUbp8uyz0kDskbu2dmz9J8\n"
+  "lSt9Ld+gn1O8cKikmxXydbH+3bhsmfwWj/lONYlcwkhL6L4bfpOxn/tD0/wBN/N944Wh9VJm/b/\n"
+  "O+347df+/rl0k+O38GLJ83X/CfTOt7v2dV6P8AMbx6njHb/pKuN3pN2+IXnaSjybr8e31fUqd+0\n"
+  "Sj487DHMryZJMXjq+sfpPX84SXk6SSX/9kAOEJJTQQhAAAAAABVAAAAAQEAAAAPAEEAZABvAGIA\n"
+  "ZQAgAFAAaABvAHQAbwBzAGgAbwBwAAAAEwBBAGQAbwBiAGUAIABQAGgAbwB0AG8AcwBoAG8AcAA\n"
+  "gADcALgAwAAAAAQA4QklNBAYAAAAAAAcABQAAAAEBAP/hFWdodHRwOi8vbnMuYWRvYmUuY29tL3\n"
+  "hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0n77u/JyBpZD0nVzVNME1wQ2VoaUh6cmVTek5UY3prY\n"
+  "zlkJz8+Cjw/YWRvYmUteGFwLWZpbHRlcnMgZXNjPSJDUiI/Pgo8eDp4YXBtZXRhIHhtbG5zOng9\n"
+  "J2Fkb2JlOm5zOm1ldGEvJyB4OnhhcHRrPSdYTVAgdG9vbGtpdCAyLjguMi0zMywgZnJhbWV3b3J\n"
+  "rIDEuNSc+CjxyZGY6UkRGIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi\n"
+  "1yZGYtc3ludGF4LW5zIycgeG1sbnM6aVg9J2h0dHA6Ly9ucy5hZG9iZS5jb20vaVgvMS4wLyc+C\n"
+  "gogPHJkZjpEZXNjcmlwdGlvbiBhYm91dD0ndXVpZDoyMmQwMmIwYS1iMjQ5LTExZGItOGFmOC05\n"
+  "MWQ1NDAzZjkyZjknCiAgeG1sbnM6cGRmPSdodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvJz4\n"
+  "KICA8IS0tIHBkZjpTdWJqZWN0IGlzIGFsaWFzZWQgLS0+CiA8L3JkZjpEZXNjcmlwdGlvbj4KCi\n"
+  "A8cmRmOkRlc2NyaXB0aW9uIGFib3V0PSd1dWlkOjIyZDAyYjBhLWIyNDktMTFkYi04YWY4LTkxZ\n"
+  "DU0MDNmOTJmOScKICB4bWxuczpwaG90b3Nob3A9J2h0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9z\n"
+  "aG9wLzEuMC8nPgogIDwhLS0gcGhvdG9zaG9wOkNhcHRpb24gaXMgYWxpYXNlZCAtLT4KIDwvcmR\n"
+  "mOkRlc2NyaXB0aW9uPgoKIDxyZGY6RGVzY3JpcHRpb24gYWJvdXQ9J3V1aWQ6MjJkMDJiMGEtYj\n"
+  "I0OS0xMWRiLThhZjgtOTFkNTQwM2Y5MmY5JwogIHhtbG5zOnhhcD0naHR0cDovL25zLmFkb2JlL\n"
+  "mNvbS94YXAvMS4wLyc+CiAgPCEtLSB4YXA6RGVzY3JpcHRpb24gaXMgYWxpYXNlZCAtLT4KIDwv\n"
+  "cmRmOkRlc2NyaXB0aW9uPgoKIDxyZGY6RGVzY3JpcHRpb24gYWJvdXQ9J3V1aWQ6MjJkMDJiMGE\n"
+  "tYjI0OS0xMWRiLThhZjgtOTFkNTQwM2Y5MmY5JwogIHhtbG5zOnhhcE1NPSdodHRwOi8vbnMuYW\n"
+  "RvYmUuY29tL3hhcC8xLjAvbW0vJz4KICA8eGFwTU06RG9jdW1lbnRJRD5hZG9iZTpkb2NpZDpwa\n"
+  "G90b3Nob3A6MjJkMDJiMDYtYjI0OS0xMWRiLThhZjgtOTFkNTQwM2Y5MmY5PC94YXBNTTpEb2N1\n"
+  "bWVudElEPgogPC9yZGY6RGVzY3JpcHRpb24+CgogPHJkZjpEZXNjcmlwdGlvbiBhYm91dD0ndXV\n"
+  "pZDoyMmQwMmIwYS1iMjQ5LTExZGItOGFmOC05MWQ1NDAzZjkyZjknCiAgeG1sbnM6ZGM9J2h0dH\n"
+  "A6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvJz4KICA8ZGM6ZGVzY3JpcHRpb24+CiAgIDxyZ\n"
+  "GY6QWx0PgogICAgPHJkZjpsaSB4bWw6bGFuZz0neC1kZWZhdWx0Jz4gICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgIDwvcmRmOkFsdD4KICA8L2RjOmRlc2NyaXB0aW9\n"
+  "uPgogPC9yZGY6RGVzY3JpcHRpb24+Cgo8L3JkZjpSREY+CjwveDp4YXBtZXRhPgogICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIA\n"
+  "ogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\n"
+  "ICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI\n"
+  "CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAg\n"
+  "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA\n"
+  "gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgIC\n"
+  "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0ndyc/P\n"
+  "v/uAA5BZG9iZQBkQAAAAAH/2wCEAAQDAwMDAwQDAwQGBAMEBgcFBAQFBwgGBgcGBggKCAkJCQkI\n"
+  "CgoMDAwMDAoMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBAUFCAcIDwoKDxQODg4UFA4ODg4UEQwMDAw\n"
+  "MEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAGQAZAMBEQACEQEDEQ\n"
+  "H/3QAEAA3/xAGiAAAABwEBAQEBAAAAAAAAAAAEBQMCBgEABwgJCgsBAAICAwEBAQEBAAAAAAAAA\n"
+  "AEAAgMEBQYHCAkKCxAAAgEDAwIEAgYHAwQCBgJzAQIDEQQABSESMUFRBhNhInGBFDKRoQcVsUIj\n"
+  "wVLR4TMWYvAkcoLxJUM0U5KismNzwjVEJ5OjszYXVGR0w9LiCCaDCQoYGYSURUaktFbTVSga8uP\n"
+  "zxNTk9GV1hZWltcXV5fVmdoaWprbG1ub2N0dXZ3eHl6e3x9fn9zhIWGh4iJiouMjY6PgpOUlZaX\n"
+  "mJmam5ydnp+So6SlpqeoqaqrrK2ur6EQACAgECAwUFBAUGBAgDA20BAAIRAwQhEjFBBVETYSIGc\n"
+  "YGRMqGx8BTB0eEjQhVSYnLxMyQ0Q4IWklMlomOywgdz0jXiRIMXVJMICQoYGSY2RRonZHRVN/Kj\n"
+  "s8MoKdPj84SUpLTE1OT0ZXWFlaW1xdXl9UZWZnaGlqa2xtbm9kdXZ3eHl6e3x9fn9zhIWGh4iJi\n"
+  "ouMjY6Pg5SVlpeYmZqbnJ2en5KjpKWmp6ipqqusra6vr/2gAMAwEAAhEDEQA/APBnplwPAdR+GB\n"
+  "KY6dYtNG1w39yh4+xb+zIksgEfFaRSSoIx8f7RPRRkSWQimM+lRmwWVXFWYigHxUUVoMiJM+Fj0\n"
+  "tg0RBegLE0Wu+3c+GTBazFCGI7HtSp9slbFYYzyoBsegw2hY1Afl3wqqRqahk+0tDgKpgu4DAUU\n"
+  "+HY+GRS2ePiMKtUB3G+KGuONq//Q8OzpFbW5WnxMop4k9crG5ZnZNJkEOn21utVRYw7HxZtz+OR\n"
+  "vdsrZ2lRtci4aVxFEQA0neg/ZXxJpTITNNuOFss0vSotYNvZ2qGRkPKSTqiU8Sdqk5SZU5Ix8XJ\n"
+  "NNZ8k6bp8TtM73OputUtYq0Unux/hkRkJOzZLCAN2KR+VpbtSkCBaDnIzdlWu59u+XeJTjeASk8\n"
+  "+juZOESEAVqx8BvU/PJibScTrTy09560hkWOGFd2YgFnPQKD19zhOSkxw2l8Vm6XAiYb8gg+k5O\n"
+  "9mnhoon9H3cs5s7WF5pp29OGGMFndyaAKBuTiEEPQLD8h/NDmNdYlttNkYjlbFjcXCr3LLH8II8\n"
+  "C2WUGviZvon/OPWkm3RNSv72SYllMkKxQRV67CQMSKYQAxMkR/wBC56d61P0heel4cYuVOXWvTp\n"
+  "h4Qjjf/9Hw5qBYyISaqjBV+QpvkAzKcki4HomnIxck/wBhtlR2bhunvlDywddMUl4zW+kQ9FQ8X\n"
+  "nfuSewrtmPkycPvc/DhMhvyegXOrWWhmLQPKlsj6xIAiLCoZkY96nv7npmJvI2XOjQFMl0fyRqM\n"
+  "NoxvZvrGt33wlATwiMnVnY1LEdSfuyXF3KIDmUu88w2XlnTl8raAlb2ZFfVL0jdYRtQnxc7BfDC\n"
+  "OaJR7nm3me5tdOtjbMvp3ZRXkV6chVQRX79hmVjgZG+jgZ5jHGhzecXF5LPL6jEjstSSaDM51Ka\n"
+  "6MZ9S1C0sEBe8uZo4YCBXdjxGw60wEWyEqfUHkT8vLXRJFuLdTcaqfhlvWUErtukZ3ABPUjIXTE\n"
+  "m3rGmeV2Tk5UKz/AG/E/wAcgZKya20C3b02kjYtH8AqCygbkUH0nLYlgUb+gbWtPbpXt/n2ybB/\n"
+  "/9Lw4oaVxGd+PxH3qBkGaY3KyiSP01IkiUclH8sg+LKydm6INvZvKsFu+kWtvD8LRoFNRup6moO\n"
+  "aqd277HsGW+XPLmn6XM17FF6l7vW4fd2Zuu+RFls2tmUNrLJb7TSBertGQGqetDkxE0na0pvtHs\n"
+  "QkszWyiGAG5laYlnkeMVHJj8sA5rPk+SvMepTalqlxd3B5zTOXdj/MxqafLpm5xioh5nPK5kpRG\n"
+  "pkcKAST0A6k5NpfUP5K/ki1ssHmHzF+71KRQ8Nud/Qibb/kYw6/yjbrXISlSH07YaHbWyxx2kXE\n"
+  "KACB2zHJtLI7XSelBRvH2xCpvaaTDHXkOTVBPcUG2479RlsdmJVPRtvV+ylenQ0y62FP/9PxRpo\n"
+  "WG5FxKKxKFDA+GVS5NsebLdFsRePc3siVW4f4QR0QVAGYeSXR2unhtZ6s60K6jt+MMSFwtF2+xX\n"
+  "wr7eGUGLlRPQMsE2vxQm7itxKg3VCfT2+nb8cDYaCDtfOXmCCcROrQrUhkkCHYn6emRMqZxjbLd\n"
+  "F1+W/4xajHzjNCtQKMffETWUdngX5p+QZ9A8xS6hbo0ui37NNDPT7DOalHpsCD08Rmyw5ARTpdV\n"
+  "gIPEF35MeRn80ed4S5EdrpKm9kZ15K0iH92hB7Me/tmS60vt/QrCYyekiBdgSTXcjqV9q9MokFD\n"
+  "N7S3aFVVR8RoK9zldqndvAY6nffr/AGYQqLhjdpCoIAZW22HavU/LJBUP9WblX0xTw7fOmWsX/9\n"
+  "Tw7FdvMqWkQ3Z1qfED+mQIbI77PX/LFis9vBajZm2Y+x65rMh3t30Bsze400aVaIbSLk6r8CMRT\n"
+  "l/NmOcllnGDD9Y8uecNfEEiXrMgDGWAyGOOu5WlB+vMrHODTlxZCdjsyFdB006VpVtLasurQxBL\n"
+  "64WiLI4/aFT1ANOXemV5piR2b9NiljB4yyHy9CLOVI5GJhB+CvXY9R8xmINzs5HNZ+Z96BZpbxA\n"
+  "fVJo39UFefwopYgL4nMiMd2qZoIn/AJx00u3t/Lt7qpp9Yv5GLf5MUTERqfbvmzBeezjd9H+VlL\n"
+  "wSQzBqsvOGQD7L12rXsemPNxmXQSxxIPU2nFV4HYqR1xEUWj4ZAxBryr2G+J2VGDZlLrxUH6KZA\n"
+  "Fkqb15VFelfwy+2FP8A/9Xxlf6AdA182Yk9eFeLxSjoVfcfSMo4uIOfkweFOnpvlWYrLEwNFAA+\n"
+  "nMOYdrhFvQLeSO7coBXiK8iKiv07Zj8Ac4QtNrW1njUcKcT+yAR/xGmR4WcsStLpTuPU9IFaEsV\n"
+  "BP3k4m2AgBzSwyQNcIwNTE1aI3wnam9O2Ug7s5Ckk/NDndeVXa2H78MqqV6jmeBp9+ZWKXqDjZ4\n"
+  "+gvVvy30qCy0qzsLRBCnBI2VdgUTqPvOZ7y+Q7pz+bn5q6d+VflZxZlJ/NN4ypptk5qtB9qRwDX\n"
+  "gn/AAx2y2ItpfKFv+eH5qNeTajJ5ovVaVywSqvEtTUKqupAA6D2y0BNPtv/AJx//M5PzL8mJeXT\n"
+  "L+ndPf6rqarSpkAqsnEAAeoN6DpkJRYci9lROSgSUUH9o9K5Tw0ztfSHnXkOtK9q+PHwydq//9b\n"
+  "yxrVoZNBtNSA5zRMPXmH8j0CLXuBmHE+qneamHpEuqYeV7pzFVTRgQK5XMNmnlb1vyyY5QA1OwJ\n"
+  "+eUF2seTOLu5s7azVIVAkpVn/hhnIALG73Yz5jvb1dICqzpDNIqyFD8SxH7R28cxibZCiWOsdJs\n"
+  "PTM6XNstPhnkjIhcHuJBVfvOCiUSn0TfWrTTLjyw8guA/PifTO3xcxxA8a5ZAbimvJP0m3p/kFF\n"
+  "WxhmpWQJ9NW3zZPHz5vlb/nIDVbrWfzO1RJhxGnpDaRL/khA1T7ktmSOTAJhZaAUtLawsbayl8v\n"
+  "xWi3Gpay0cF3HPcFRJJHJMXVrcJ8UaAFG5LWjF8tAYW9H/wCcOo9bTzxrt/owkTyksZW5gkIKvI\n"
+  "7k26nvyReRJHyyBWT7dWQyOWlbnK2526e1O1MqIUFE84uPLkOdK9RXI0E2/wD/1/DA1bURZLY/W\n"
+  "ZDZqwb0eXw7dMgIi7bjllVXsz7yNcfWC0Vd3Ip92Y2UOz0cnsPlwyx8xQ/u24sMxCadoJp9LOXk\n"
+  "VX/uwRUE0BI8cokbLMyoKouHu2MaKGXw7fLDwgoGSkbHpaNZyLLHRSKcFFQQRvUdMlwUFOQyLzr\n"
+  "ztpCaba6fPau4ijv4OURY8AjVFKV7ZZiO+7Vnh6XvXkSWNbW2WTb92KDxIFMzwHlZc3zX+fuizW\n"
+  "f5p3ty8XGDU4YLmCQiisyII3+4rvl8UB5ffEghRGvOm7AbnvWvjk1fen/ONPldPKP5aWOpPCfr2\n"
+  "uE31y6q2wbaMEn+VAMDSdyzrzj+avlHyTp0l/r2rxWFuHWJuIeacu4qFCRgsajfBwsty89/6Gr/\n"
+  "ACa9an+JL/hSnrfoubhXwpXpjwhaL//Q8E1AqtcAZMs8l6i1nqMa1oSVP0VynKLDmaWdSfQXl69\n"
+  "jF1Jv8MhDb5rpB3AO7INRRLhhGp4R05FgaGvTMU8200xS70zVDMRp2pTIOvBmB3PgQP15kxIcnD\n"
+  "LH/EEz0rRvOJhldr9pQtCqyd6VrShGTqw5d4ARv9jHfOGl+ZJNMluLkyenaFbiRdqFYW5nrWuwO\n"
+  "MKB5MdSMRxnhlu9N8p6lLFpti63FUjCtFJTrDKvse2bEDZ4XJ9RZB+YPli2/Mjy5bxoUi1a0YS2\n"
+  "85UOwIXiy9jRu+TBppfOF1+V3m22vrdpNPM8cs/oo0VJlUqQPjValR3+IZNNvtLS9Yu9Mi0/TJr\n"
+  "kyp6QhWVVCIWRATsKBemwwFrDzT87fybs/wA1bW21PRb+DTvNlgGSRp6iC8i3KJJx+y6n7D0Pwm\n"
+  "hxBZXT55/6Fi/Nf0PW+qWXq+t6X1X67F6vD/ftK04V/wBl344U8b//0fBapxheVh9ocV+nviqY2\n"
+  "/qQJDew/bioWHiuQ8m0bbvaPKGtQ6jaxSo9JloCK75gZI0Xb4sgkHo8MouoAvP94BsRmGY7uWJU\n"
+  "gzbypOQpNOvIdK4Nw2WCE2tXulTkjEEbdafgclxMhFBas93dwyQzsWDghlJFONKHJCZtjOFBJfy\n"
+  "j1y9vPL9zpbIs0WkXL2sUjA8hDXlGCRXtt07ZuYvL5KJeo6bfajbkzWkcToR8dqshZ6in2fhNK/\n"
+  "PDTUlXmHVvMdr5o0v9H2kdrqGpfu7m0nkY87Uf7tkKAU4/s03ynLkEBbfihx7dGT6va67LbRMNR\n"
+  "aKOBuUTKgIBXoK1BOYR1M3aQ0mOt9yxUeZNdtJhFapLqMluSXkg5oxJrUMW5KevQ9MmNXXNqOiH\n"
+  "Rr/Hmv8A1r9I/oj95w+r+j9Yf1+NP5+nXtTD+dF8tkfkOlv/0vC3ph7f0/alcVTbS4A8QibuKb5\n"
+  "RI05EBYRFpdX3ly79a2qYCavH/EY7TCYyMD5PSdD8+wXUSn1ArDqOhBzFlipz4ZwWbaV5htbsgF\n"
+  "qg9crMXKErGyYwajFGzxyHlGSePbbwyqg5UZlCaxrFpaWU95LIqrEjMAT4Dp9OShGy1ZslBhv/A\n"
+  "Dj9rd/a+aL+xUK+m38L3d0HrxRo2HFtu5D8c27y8t30raarbWkU+u6g4gsNORn+EcUaSh2Pc0/4\n"
+  "lgtAjezzbT9SutY1i782al8Nxdyotqh6xWybIg+jc5q8s+I27bFDgFPQp9RE+nrag70+L6crrZu\n"
+  "4jajokdv6LW/Dii1Wo61PXKQN3KPK0L+h4/rnD/K5V78a5LhXxd3/0/DMXXtwxVNtL9Xkaf3f7N\n"
+  "etfbKMjdjtkZ9D6ufrlK0+HpX8coF9HJ26sXvfqXrf7i/U+uften/d/wCyrmQL6uOav0pvpP8Ai\n"
+  "b1F+rV59+vH6a5XLhcjH4nRmY/xpxHP0/UptWvT6Mx/RbmjxWK+aP8AFf1M/pCv1Kvxen9inavf\n"
+  "MrFwXtzcLUeLXq5Mv/I3nz1b0v8AjofuKVry9KrUpTanOlf9jmQ68va/zH9b/COn/o7/AI431mP\n"
+  "65SvLh+zWvbl9rMfNfC34K4kmj9T6lD6FKclp/DNYXZx5srsPrHor6nXvkgxTPS/U+rv6dPU5mt\n"
+  "fngFN5ulv+l/pL/Lp/scerHo//2Q==\n";
+
+static std::string gCommandLine;
+
+TEST(Base64, LargeSample) {
+  LOG(LS_VERBOSE) << "Testing specific base64 file";
+
+  char unescaped[64 * 1024];
+
+  // unescape that massive blob above
+  size_t size = Base64Unescape(SpecificTest,
+                            sizeof(SpecificTest),
+                            unescaped,
+                            sizeof(unescaped));
+
+  EXPECT_EQ(size, sizeof(testbase64));
+  EXPECT_EQ(0, memcmp(testbase64, unescaped, sizeof(testbase64)));
+}
+
+bool DecodeTest(const char* encoded, size_t expect_unparsed,
+                const char* decoded, Base64::DecodeFlags flags)
+{
+  std::string result;
+  size_t consumed = 0, encoded_len = strlen(encoded);
+  bool success = Base64::DecodeFromArray(encoded, encoded_len, flags,
+                                         &result, &consumed);
+  size_t unparsed = encoded_len - consumed;
+  EXPECT_EQ(expect_unparsed, unparsed) << "\"" << encoded
+                                       << "\" -> \"" << decoded
+                                       << "\"";
+  EXPECT_STREQ(decoded, result.c_str());
+  return success;
+}
+
+#define Flags(x,y,z) \
+  Base64::DO_PARSE_##x | Base64::DO_PAD_##y | Base64::DO_TERM_##z
+
+TEST(Base64, DecodeParseOptions) {
+  // Trailing whitespace
+  EXPECT_TRUE (DecodeTest("YWJjZA== ", 1, "abcd", Flags(STRICT, YES, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA== ", 0, "abcd", Flags(WHITE,  YES, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA== ", 0, "abcd", Flags(ANY,    YES, CHAR)));
+
+  // Embedded whitespace
+  EXPECT_FALSE(DecodeTest("YWJjZA= =", 3, "abcd", Flags(STRICT, YES, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA= =", 0, "abcd", Flags(WHITE,  YES, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA= =", 0, "abcd", Flags(ANY,    YES, CHAR)));
+
+  // Embedded non-base64 characters
+  EXPECT_FALSE(DecodeTest("YWJjZA=*=", 3, "abcd", Flags(STRICT, YES, CHAR)));
+  EXPECT_FALSE(DecodeTest("YWJjZA=*=", 3, "abcd", Flags(WHITE,  YES, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA=*=", 0, "abcd", Flags(ANY,    YES, CHAR)));
+
+  // Unexpected padding characters
+  EXPECT_FALSE(DecodeTest("YW=JjZA==", 7, "a",    Flags(STRICT, YES, CHAR)));
+  EXPECT_FALSE(DecodeTest("YW=JjZA==", 7, "a",    Flags(WHITE,  YES, CHAR)));
+  EXPECT_TRUE (DecodeTest("YW=JjZA==", 0, "abcd", Flags(ANY,    YES, CHAR)));
+}
+
+TEST(Base64, DecodePadOptions) {
+  // Padding
+  EXPECT_TRUE (DecodeTest("YWJjZA==",  0, "abcd", Flags(STRICT, YES, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA==",  0, "abcd", Flags(STRICT, ANY, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA==",  2, "abcd", Flags(STRICT, NO,  CHAR)));
+
+  // Incomplete padding
+  EXPECT_FALSE(DecodeTest("YWJjZA=",   1, "abcd", Flags(STRICT, YES, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA=",   1, "abcd", Flags(STRICT, ANY, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA=",   1, "abcd", Flags(STRICT, NO,  CHAR)));
+
+  // No padding
+  EXPECT_FALSE(DecodeTest("YWJjZA",    0, "abcd", Flags(STRICT, YES, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA",    0, "abcd", Flags(STRICT, ANY, CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJjZA",    0, "abcd", Flags(STRICT, NO,  CHAR)));
+}
+
+TEST(Base64, DecodeTerminateOptions) {
+  // Complete quantum
+  EXPECT_TRUE (DecodeTest("YWJj",      0, "abc",  Flags(STRICT, NO,  BUFFER)));
+  EXPECT_TRUE (DecodeTest("YWJj",      0, "abc",  Flags(STRICT, NO,  CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJj",      0, "abc",  Flags(STRICT, NO,  ANY)));
+
+  // Complete quantum with trailing data
+  EXPECT_FALSE(DecodeTest("YWJj*",     1, "abc",  Flags(STRICT, NO,  BUFFER)));
+  EXPECT_TRUE (DecodeTest("YWJj*",     1, "abc",  Flags(STRICT, NO,  CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJj*",     1, "abc",  Flags(STRICT, NO,  ANY)));
+
+  // Incomplete quantum
+  EXPECT_FALSE(DecodeTest("YWJ",       0, "ab",   Flags(STRICT, NO,  BUFFER)));
+  EXPECT_FALSE(DecodeTest("YWJ",       0, "ab",   Flags(STRICT, NO,  CHAR)));
+  EXPECT_TRUE (DecodeTest("YWJ",       0, "ab",   Flags(STRICT, NO,  ANY)));
+}
diff --git a/talk/base/dbus.cc b/talk/base/dbus.cc
new file mode 100644
index 0000000..86fea4d
--- /dev/null
+++ b/talk/base/dbus.cc
@@ -0,0 +1,405 @@
+/*
+ * libjingle
+ * Copyright 2004--2011, 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.
+ */
+
+#ifdef HAVE_DBUS_GLIB
+
+#include "talk/base/dbus.h"
+
+#include <dbus/dbus-glib-lowlevel.h>
+
+#include "talk/base/thread.h"
+
+namespace talk_base {
+
+// Avoid static object construction/destruction on startup/shutdown.
+static pthread_once_t g_dbus_init_once = PTHREAD_ONCE_INIT;
+static LibDBusGlibSymbolTable *g_dbus_symbol = NULL;
+
+// Releases DBus-Glib symbols.
+static void ReleaseDBusGlibSymbol() {
+  if (g_dbus_symbol != NULL) {
+    delete g_dbus_symbol;
+    g_dbus_symbol = NULL;
+  }
+}
+
+// Loads DBus-Glib symbols.
+static void InitializeDBusGlibSymbol() {
+  // This is thread safe.
+  if (NULL == g_dbus_symbol) {
+    g_dbus_symbol = new LibDBusGlibSymbolTable();
+
+    // Loads dbus-glib
+    if (NULL == g_dbus_symbol || !g_dbus_symbol->Load()) {
+      LOG(LS_WARNING) << "Failed to load dbus-glib symbol table.";
+      ReleaseDBusGlibSymbol();
+    } else {
+      // Nothing we can do if atexit() failed. Just ignore its returned value.
+      atexit(ReleaseDBusGlibSymbol);
+    }
+  }
+}
+
+// Returns a reference to the given late-binded symbol, with the correct type.
+#define LATE(sym) LATESYM_GET(LibDBusGlibSymbolTable, \
+    DBusMonitor::GetDBusGlibSymbolTable(), sym)
+
+// Implementation of class DBusSigMessageData
+DBusSigMessageData::DBusSigMessageData(DBusMessage *message)
+    : TypedMessageData<DBusMessage *>(message) {
+  LATE(dbus_message_ref)(data());
+}
+
+DBusSigMessageData::~DBusSigMessageData() {
+  LATE(dbus_message_unref)(data());
+}
+
+// Implementation of class DBusSigFilter
+
+// Builds a DBus filter string from given DBus path, interface and member.
+std::string DBusSigFilter::BuildFilterString(const std::string &path,
+                                             const std::string &interface,
+                                             const std::string &member) {
+  std::string ret(DBUS_TYPE "='" DBUS_SIGNAL "'");
+  if (!path.empty()) {
+    ret += ("," DBUS_PATH "='");
+    ret += path;
+    ret += "'";
+  }
+  if (!interface.empty()) {
+    ret += ("," DBUS_INTERFACE "='");
+    ret += interface;
+    ret += "'";
+  }
+  if (!member.empty()) {
+    ret += ("," DBUS_MEMBER "='");
+    ret += member;
+    ret += "'";
+  }
+  return ret;
+}
+
+// Forwards the message to the given instance.
+DBusHandlerResult DBusSigFilter::DBusCallback(DBusConnection *dbus_conn,
+                                              DBusMessage *message,
+                                              void *instance) {
+  ASSERT(instance);
+  if (instance) {
+    return static_cast<DBusSigFilter *>(instance)->Callback(message);
+  }
+  return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+// Posts a message to caller thread.
+DBusHandlerResult DBusSigFilter::Callback(DBusMessage *message) {
+  if (caller_thread_) {
+    caller_thread_->Post(this, DSM_SIGNAL, new DBusSigMessageData(message));
+  }
+  // Don't "eat" the message here. Let it pop up.
+  return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+// From MessageHandler.
+void DBusSigFilter::OnMessage(Message *message) {
+  if (message != NULL && DSM_SIGNAL == message->message_id) {
+    DBusSigMessageData *msg =
+        static_cast<DBusSigMessageData *>(message->pdata);
+    if (msg) {
+      ProcessSignal(msg->data());
+      delete msg;
+    }
+  }
+}
+
+// Definition of private class DBusMonitoringThread.
+// It creates a worker-thread to listen signals on DBus. The worker-thread will
+// be running in a priate GMainLoop forever until either Stop() has been invoked
+// or it hits an error.
+class DBusMonitor::DBusMonitoringThread : public talk_base::Thread {
+ public:
+  explicit DBusMonitoringThread(DBusMonitor *monitor,
+                                GMainContext *context,
+                                GMainLoop *mainloop,
+                                std::vector<DBusSigFilter *> *filter_list)
+      : monitor_(monitor),
+        context_(context),
+        mainloop_(mainloop),
+        connection_(NULL),
+        idle_source_(NULL),
+        filter_list_(filter_list) {
+    ASSERT(monitor_);
+    ASSERT(context_);
+    ASSERT(mainloop_);
+    ASSERT(filter_list_);
+  }
+
+  // Override virtual method of Thread. Context: worker-thread.
+  virtual void Run() {
+    ASSERT(NULL == connection_);
+
+    // Setup DBus connection and start monitoring.
+    monitor_->OnMonitoringStatusChanged(DMS_INITIALIZING);
+    if (!Setup()) {
+      LOG(LS_ERROR) << "DBus monitoring setup failed.";
+      monitor_->OnMonitoringStatusChanged(DMS_FAILED);
+      CleanUp();
+      return;
+    }
+    monitor_->OnMonitoringStatusChanged(DMS_RUNNING);
+    LATE(g_main_loop_run)(mainloop_);
+    monitor_->OnMonitoringStatusChanged(DMS_STOPPED);
+
+    // Done normally. Clean up DBus connection.
+    CleanUp();
+    return;
+  }
+
+  // Override virtual method of Thread. Context: caller-thread.
+  virtual void Stop() {
+    ASSERT(NULL == idle_source_);
+    // Add an idle source and let the gmainloop quit on idle.
+    idle_source_ = LATE(g_idle_source_new)();
+    if (idle_source_) {
+      LATE(g_source_set_callback)(idle_source_, &Idle, this, NULL);
+      LATE(g_source_attach)(idle_source_, context_);
+    } else {
+      LOG(LS_ERROR) << "g_idle_source_new() failed.";
+      QuitGMainloop();  // Try to quit anyway.
+    }
+
+    Thread::Stop();  // Wait for the thread.
+  }
+
+ private:
+  // Registers all DBus filters.
+  void RegisterAllFilters() {
+    ASSERT(NULL != LATE(dbus_g_connection_get_connection)(connection_));
+
+    for (std::vector<DBusSigFilter *>::iterator it = filter_list_->begin();
+         it != filter_list_->end(); ++it) {
+      DBusSigFilter *filter = (*it);
+      if (!filter) {
+        LOG(LS_ERROR) << "DBusSigFilter list corrupted.";
+        continue;
+      }
+
+      LATE(dbus_bus_add_match)(
+          LATE(dbus_g_connection_get_connection)(connection_),
+          filter->filter().c_str(), NULL);
+
+      if (!LATE(dbus_connection_add_filter)(
+              LATE(dbus_g_connection_get_connection)(connection_),
+              &DBusSigFilter::DBusCallback, filter, NULL)) {
+        LOG(LS_ERROR) << "dbus_connection_add_filter() failed."
+                      << "Filter: " << filter->filter();
+        continue;
+      }
+    }
+  }
+
+  // Unregisters all DBus filters.
+  void UnRegisterAllFilters() {
+    ASSERT(NULL != LATE(dbus_g_connection_get_connection)(connection_));
+
+    for (std::vector<DBusSigFilter *>::iterator it = filter_list_->begin();
+         it != filter_list_->end(); ++it) {
+      DBusSigFilter *filter = (*it);
+      if (!filter) {
+        LOG(LS_ERROR) << "DBusSigFilter list corrupted.";
+        continue;
+      }
+      LATE(dbus_connection_remove_filter)(
+          LATE(dbus_g_connection_get_connection)(connection_),
+          &DBusSigFilter::DBusCallback, filter);
+    }
+  }
+
+  // Sets up the monitoring thread.
+  bool Setup() {
+    LATE(g_main_context_push_thread_default)(context_);
+
+    // Start connection to dbus.
+    // If dbus daemon is not running, returns false immediately.
+    connection_ = LATE(dbus_g_bus_get_private)(monitor_->type_, context_, NULL);
+    if (NULL == connection_) {
+      LOG(LS_ERROR) << "dbus_g_bus_get_private() unable to get connection.";
+      return false;
+    }
+    if (NULL == LATE(dbus_g_connection_get_connection)(connection_)) {
+      LOG(LS_ERROR) << "dbus_g_connection_get_connection() returns NULL. "
+                    << "DBus daemon is probably not running.";
+      return false;
+    }
+
+    // Application don't exit if DBus daemon die.
+    LATE(dbus_connection_set_exit_on_disconnect)(
+        LATE(dbus_g_connection_get_connection)(connection_), FALSE);
+
+    // Connect all filters.
+    RegisterAllFilters();
+
+    return true;
+  }
+
+  // Cleans up the monitoring thread.
+  void CleanUp() {
+    if (idle_source_) {
+      // We did an attach() with the GSource, so we need to destroy() it.
+      LATE(g_source_destroy)(idle_source_);
+      // We need to unref() the GSource to end the last reference we got.
+      LATE(g_source_unref)(idle_source_);
+      idle_source_ = NULL;
+    }
+    if (connection_) {
+      if (LATE(dbus_g_connection_get_connection)(connection_)) {
+        UnRegisterAllFilters();
+        LATE(dbus_connection_close)(
+            LATE(dbus_g_connection_get_connection)(connection_));
+      }
+      LATE(dbus_g_connection_unref)(connection_);
+      connection_ = NULL;
+    }
+    LATE(g_main_loop_unref)(mainloop_);
+    mainloop_ = NULL;
+    LATE(g_main_context_unref)(context_);
+    context_ = NULL;
+  }
+
+  // Handles callback on Idle. We only add this source when ready to stop.
+  static gboolean Idle(gpointer data) {
+    static_cast<DBusMonitoringThread *>(data)->QuitGMainloop();
+    return TRUE;
+  }
+
+  // We only hit this when ready to quit.
+  void QuitGMainloop() {
+    LATE(g_main_loop_quit)(mainloop_);
+  }
+
+  DBusMonitor *monitor_;
+
+  GMainContext *context_;
+  GMainLoop *mainloop_;
+  DBusGConnection *connection_;
+  GSource *idle_source_;
+
+  std::vector<DBusSigFilter *> *filter_list_;
+};
+
+// Implementation of class DBusMonitor
+
+// Returns DBus-Glib symbol handle. Initialize it first if hasn't.
+LibDBusGlibSymbolTable *DBusMonitor::GetDBusGlibSymbolTable() {
+  // This is multi-thread safe.
+  pthread_once(&g_dbus_init_once, InitializeDBusGlibSymbol);
+
+  return g_dbus_symbol;
+};
+
+// Creates an instance of DBusMonitor
+DBusMonitor *DBusMonitor::Create(DBusBusType type) {
+  if (NULL == DBusMonitor::GetDBusGlibSymbolTable()) {
+    return NULL;
+  }
+  return new DBusMonitor(type);
+}
+
+DBusMonitor::DBusMonitor(DBusBusType type)
+    : type_(type),
+      status_(DMS_NOT_INITIALIZED),
+      monitoring_thread_(NULL) {
+  ASSERT(type_ == DBUS_BUS_SYSTEM || type_ == DBUS_BUS_SESSION);
+}
+
+DBusMonitor::~DBusMonitor() {
+  StopMonitoring();
+}
+
+bool DBusMonitor::AddFilter(DBusSigFilter *filter) {
+  if (monitoring_thread_) {
+    return false;
+  }
+  if (!filter) {
+    return false;
+  }
+  filter_list_.push_back(filter);
+  return true;
+}
+
+bool DBusMonitor::StartMonitoring() {
+  if (!monitoring_thread_) {
+    LATE(g_type_init)();
+    LATE(g_thread_init)(NULL);
+    LATE(dbus_g_thread_init)();
+
+    GMainContext *context = LATE(g_main_context_new)();
+    if (NULL == context) {
+      LOG(LS_ERROR) << "g_main_context_new() failed.";
+      return false;
+    }
+
+    GMainLoop *mainloop = LATE(g_main_loop_new)(context, FALSE);
+    if (NULL == mainloop) {
+      LOG(LS_ERROR) << "g_main_loop_new() failed.";
+      LATE(g_main_context_unref)(context);
+      return false;
+    }
+
+    monitoring_thread_ = new DBusMonitoringThread(this, context, mainloop,
+                                                  &filter_list_);
+    if (monitoring_thread_ == NULL) {
+      LOG(LS_ERROR) << "Failed to create DBus monitoring thread.";
+      LATE(g_main_context_unref)(context);
+      LATE(g_main_loop_unref)(mainloop);
+      return false;
+    }
+    monitoring_thread_->Start();
+  }
+  return true;
+}
+
+bool DBusMonitor::StopMonitoring() {
+  if (monitoring_thread_) {
+    monitoring_thread_->Stop();
+    monitoring_thread_ = NULL;
+  }
+  return true;
+}
+
+DBusMonitor::DBusMonitorStatus DBusMonitor::GetStatus() {
+  return status_;
+}
+
+void DBusMonitor::OnMonitoringStatusChanged(DBusMonitorStatus status) {
+  status_ = status;
+}
+
+#undef LATE
+
+}  // namespace talk_base
+
+#endif  // HAVE_DBUS_GLIB
diff --git a/talk/base/dbus.h b/talk/base/dbus.h
new file mode 100644
index 0000000..7dce350
--- /dev/null
+++ b/talk/base/dbus.h
@@ -0,0 +1,185 @@
+/*
+ * libjingle
+ * Copyright 2004--2011, 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.
+ */
+
+#ifndef TALK_BASE_DBUS_H_
+#define TALK_BASE_DBUS_H_
+
+#ifdef HAVE_DBUS_GLIB
+
+#include <dbus/dbus.h>
+
+#include <string>
+#include <vector>
+
+#include "talk/base/libdbusglibsymboltable.h"
+#include "talk/base/messagehandler.h"
+#include "talk/base/thread.h"
+
+namespace talk_base {
+
+#define DBUS_TYPE                   "type"
+#define DBUS_SIGNAL                 "signal"
+#define DBUS_PATH                   "path"
+#define DBUS_INTERFACE              "interface"
+#define DBUS_MEMBER                 "member"
+
+#ifdef CHROMEOS
+#define CROS_PM_PATH                "/"
+#define CROS_PM_INTERFACE           "org.chromium.PowerManager"
+#define CROS_SIG_POWERCHANGED       "PowerStateChanged"
+#define CROS_VALUE_SLEEP            "mem"
+#define CROS_VALUE_RESUME           "on"
+#else
+#define UP_PATH                     "/org/freedesktop/UPower"
+#define UP_INTERFACE                "org.freedesktop.UPower"
+#define UP_SIG_SLEEPING             "Sleeping"
+#define UP_SIG_RESUMING             "Resuming"
+#endif  // CHROMEOS
+
+// Wraps a DBus messages.
+class DBusSigMessageData : public TypedMessageData<DBusMessage *> {
+ public:
+  explicit DBusSigMessageData(DBusMessage *message);
+  ~DBusSigMessageData();
+};
+
+// DBusSigFilter is an abstract class that defines the interface of DBus
+// signal handling.
+// The subclasses implement ProcessSignal() for various purposes.
+// When a DBus signal comes, a DSM_SIGNAL message is posted to the caller thread
+// which will then invokes ProcessSignal().
+class DBusSigFilter : protected MessageHandler {
+ public:
+  enum DBusSigMessage { DSM_SIGNAL };
+
+  // This filter string should ususally come from BuildFilterString()
+  explicit DBusSigFilter(const std::string &filter)
+      : caller_thread_(Thread::Current()), filter_(filter) {
+  }
+
+  // Builds a DBus monitor filter string from given DBus path, interface, and
+  // member.
+  // See http://dbus.freedesktop.org/doc/api/html/group__DBusConnection.html
+  static std::string BuildFilterString(const std::string &path,
+                                       const std::string &interface,
+                                       const std::string &member);
+
+  // Handles callback on DBus messages by DBus system.
+  static DBusHandlerResult DBusCallback(DBusConnection *dbus_conn,
+                                        DBusMessage *message,
+                                        void *instance);
+
+  // Handles callback on DBus messages to each DBusSigFilter instance.
+  DBusHandlerResult Callback(DBusMessage *message);
+
+  // From MessageHandler.
+  virtual void OnMessage(Message *message);
+
+  // Returns the DBus monitor filter string.
+  const std::string &filter() const { return filter_; }
+
+ private:
+  // On caller thread.
+  virtual void ProcessSignal(DBusMessage *message) = 0;
+
+  Thread *caller_thread_;
+  const std::string filter_;
+};
+
+// DBusMonitor is a class for DBus signal monitoring.
+//
+// The caller-thread calls AddFilter() first to add the signals that it wants to
+// monitor and then calls StartMonitoring() to start the monitoring.
+// This will create a worker-thread which listens on DBus connection and sends
+// DBus signals back through the callback.
+// The worker-thread will be running forever until either StopMonitoring() is
+// called from the caller-thread or the worker-thread hit some error.
+//
+// Programming model:
+//   1. Caller-thread: Creates an object of DBusMonitor.
+//   2. Caller-thread: Calls DBusMonitor::AddFilter() one or several times.
+//   3. Caller-thread: StartMonitoring().
+//      ...
+//   4. Worker-thread: DBus signal recieved. Post a message to caller-thread.
+//   5. Caller-thread: DBusFilterBase::ProcessSignal() is invoked.
+//      ...
+//   6. Caller-thread: StopMonitoring().
+//
+// Assumption:
+//   AddFilter(), StartMonitoring(), and StopMonitoring() methods are called by
+//   a single thread. Hence, there is no need to make them thread safe.
+class DBusMonitor {
+ public:
+  // Status of DBus monitoring.
+  enum DBusMonitorStatus {
+    DMS_NOT_INITIALIZED,  // Not initialized.
+    DMS_INITIALIZING,     // Initializing the monitoring thread.
+    DMS_RUNNING,          // Monitoring.
+    DMS_STOPPED,          // Not monitoring. Stopped normally.
+    DMS_FAILED,           // Not monitoring. Failed.
+  };
+
+  // Returns the DBus-Glib symbol table.
+  // We should only use this function to access DBus-Glib symbols.
+  static LibDBusGlibSymbolTable *GetDBusGlibSymbolTable();
+
+  // Creates an instance of DBusMonitor.
+  static DBusMonitor *Create(DBusBusType type);
+  ~DBusMonitor();
+
+  // Adds a filter to DBusMonitor.
+  bool AddFilter(DBusSigFilter *filter);
+
+  // Starts DBus message monitoring.
+  bool StartMonitoring();
+
+  // Stops DBus message monitoring.
+  bool StopMonitoring();
+
+  // Gets the status of DBus monitoring.
+  DBusMonitorStatus GetStatus();
+
+ private:
+  // Forward declaration. Defined in the .cc file.
+  class DBusMonitoringThread;
+
+  explicit DBusMonitor(DBusBusType type);
+
+  // Updates status_ when monitoring status has changed.
+  void OnMonitoringStatusChanged(DBusMonitorStatus status);
+
+  DBusBusType type_;
+  DBusMonitorStatus status_;
+  DBusMonitoringThread *monitoring_thread_;
+  std::vector<DBusSigFilter *> filter_list_;
+};
+
+}  // namespace talk_base
+
+#endif  // HAVE_DBUS_GLIB
+
+#endif  // TALK_BASE_DBUS_H_
diff --git a/talk/base/diskcache_win32.cc b/talk/base/diskcache_win32.cc
new file mode 100644
index 0000000..b49ed81
--- /dev/null
+++ b/talk/base/diskcache_win32.cc
@@ -0,0 +1,103 @@
+/*
+ * libjingle
+ * Copyright 2006, 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.
+ */
+
+#include "talk/base/win32.h"
+#include <shellapi.h>
+#include <shlobj.h>
+#include <tchar.h>
+
+#include <time.h>
+
+#include "talk/base/common.h"
+#include "talk/base/diskcache.h"
+#include "talk/base/pathutils.h"
+#include "talk/base/stream.h"
+#include "talk/base/stringencode.h"
+#include "talk/base/stringutils.h"
+
+#include "talk/base/diskcache_win32.h"
+
+namespace talk_base {
+
+bool DiskCacheWin32::InitializeEntries() {
+  // Note: We could store the cache information in a separate file, for faster
+  // initialization.  Figuring it out empirically works, too.
+
+  std::wstring path16 = ToUtf16(folder_);
+  path16.append(1, '*');
+
+  WIN32_FIND_DATA find_data;
+  HANDLE find_handle = FindFirstFile(path16.c_str(), &find_data);
+  if (find_handle != INVALID_HANDLE_VALUE) {
+    do {
+      size_t index;
+      std::string id;
+      if (!FilenameToId(ToUtf8(find_data.cFileName), &id, &index))
+        continue;
+
+      Entry* entry = GetOrCreateEntry(id, true);
+      entry->size += find_data.nFileSizeLow;
+      total_size_ += find_data.nFileSizeLow;
+      entry->streams = _max(entry->streams, index + 1);
+      FileTimeToUnixTime(find_data.ftLastWriteTime, &entry->last_modified);
+
+    } while (FindNextFile(find_handle, &find_data));
+
+    FindClose(find_handle);
+  }
+
+  return true;
+}
+
+bool DiskCacheWin32::PurgeFiles() {
+  std::wstring path16 = ToUtf16(folder_);
+  path16.append(1, '*');
+  path16.append(1, '\0');
+
+  SHFILEOPSTRUCT file_op = { 0 };
+  file_op.wFunc = FO_DELETE;
+  file_op.pFrom = path16.c_str();
+  file_op.fFlags = FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
+                   | FOF_NORECURSION | FOF_FILESONLY;
+  if (0 != SHFileOperation(&file_op)) {
+    LOG_F(LS_ERROR) << "Couldn't delete cache files";
+    return false;
+  }
+
+  return true;
+}
+
+bool DiskCacheWin32::FileExists(const std::string& filename) const {
+  DWORD result = ::GetFileAttributes(ToUtf16(filename).c_str());
+  return (INVALID_FILE_ATTRIBUTES != result);
+}
+
+bool DiskCacheWin32::DeleteFile(const std::string& filename) const {
+  return ::DeleteFile(ToUtf16(filename).c_str()) != 0;
+}
+
+}
diff --git a/talk/base/diskcache_win32.h b/talk/base/diskcache_win32.h
new file mode 100644
index 0000000..a5e8de5
--- /dev/null
+++ b/talk/base/diskcache_win32.h
@@ -0,0 +1,46 @@
+/*
+ * libjingle
+ * Copyright 2006, 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.
+ */
+
+#ifndef TALK_BASE_DISKCACHEWIN32_H__
+#define TALK_BASE_DISKCACHEWIN32_H__
+
+#include "talk/base/diskcache.h"
+
+namespace talk_base {
+
+class DiskCacheWin32 : public DiskCache {
+ protected:
+  virtual bool InitializeEntries();
+  virtual bool PurgeFiles();
+
+  virtual bool FileExists(const std::string& filename) const;
+  virtual bool DeleteFile(const std::string& filename) const;
+};
+
+}
+
+#endif  // TALK_BASE_DISKCACHEWIN32_H__
diff --git a/talk/base/fileutils_mock.h b/talk/base/fileutils_mock.h
index 2ccb6a3..b91e802 100644
--- a/talk/base/fileutils_mock.h
+++ b/talk/base/fileutils_mock.h
@@ -33,9 +33,9 @@
 #include <vector>
 
 #include "talk/base/fileutils.h"
+#include "talk/base/gunit.h"
 #include "talk/base/pathutils.h"
 #include "talk/base/stream.h"
-#include "testing/base/public/gmock.h"
 
 namespace talk_base {
 
@@ -168,81 +168,81 @@
     }
 
     bool CreatePrivateFile(const Pathname &filename) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool DeleteFile(const Pathname &filename) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool DeleteEmptyFolder(const Pathname &folder) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool DeleteFolderContents(const Pathname &folder) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool DeleteFolderAndContents(const Pathname &folder) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool CreateFolder(const Pathname &pathname) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool MoveFolder(const Pathname &old_path, const Pathname &new_path) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool MoveFile(const Pathname &old_path, const Pathname &new_path) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool CopyFile(const Pathname &old_path, const Pathname &new_path) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool IsFolder(const Pathname &pathname) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool IsFile(const Pathname &pathname) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool IsAbsent(const Pathname &pathname) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool IsTemporaryPath(const Pathname &pathname) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool GetTemporaryFolder(Pathname &path, bool create,
                             const std::string *append) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     std::string TempFilename(const Pathname &dir, const std::string &prefix) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return std::string();
     }
     bool GetFileSize(const Pathname &path, size_t *size) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool GetFileTime(const Pathname &path, FileTimeType which,
                      time_t* time) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool GetAppPathname(Pathname *path) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool GetAppDataFolder(Pathname *path, bool per_user) {
-      EXPECT_TRUE_M(per_user, "Unsupported operation");
+      EXPECT_TRUE(per_user) << "Unsupported operation";
 #ifdef WIN32
       path->SetPathname("c:\\Users\\test_user", "");
 #else
@@ -251,11 +251,11 @@
       return true;
     }
     bool GetAppTempFolder(Pathname *path) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     bool GetDiskFreeSpace(const Pathname &path, int64 *freebytes) {
-      EXPECT_TRUE_M(false, "Unsupported operation");
+      EXPECT_TRUE(false) << "Unsupported operation";
       return false;
     }
     Pathname GetCurrentDirectory() {
diff --git a/talk/base/json_unittest.cc b/talk/base/json_unittest.cc
new file mode 100644
index 0000000..a39b944
--- /dev/null
+++ b/talk/base/json_unittest.cc
@@ -0,0 +1,205 @@
+/*
+ * libjingle
+ * Copyright 2009, 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.
+ */
+
+#include <vector>
+#include "talk/base/gunit.h"
+#include "talk/base/json.h"
+
+static Json::Value in_s("foo");
+static Json::Value in_sn("99");
+static Json::Value in_si("-99");
+static Json::Value in_sb("true");
+static Json::Value in_sd("1.2");
+static Json::Value in_n(12);
+static Json::Value in_i(-12);
+static Json::Value in_u(34U);
+static Json::Value in_b(true);
+static Json::Value in_d(1.2);
+static Json::Value big_sn("12345678901234567890");
+static Json::Value big_si("-12345678901234567890");
+static Json::Value big_u(0xFFFFFFFF);
+static Json::Value bad_a(Json::arrayValue);
+static Json::Value bad_o(Json::objectValue);
+
+TEST(JsonTest, GetString) {
+  std::string out;
+  EXPECT_TRUE(GetStringFromJson(in_s, &out));
+  EXPECT_EQ("foo", out);
+  EXPECT_TRUE(GetStringFromJson(in_sn, &out));
+  EXPECT_EQ("99", out);
+  EXPECT_TRUE(GetStringFromJson(in_si, &out));
+  EXPECT_EQ("-99", out);
+  EXPECT_TRUE(GetStringFromJson(in_i, &out));
+  EXPECT_EQ("-12", out);
+  EXPECT_TRUE(GetStringFromJson(in_n, &out));
+  EXPECT_EQ("12", out);
+  EXPECT_TRUE(GetStringFromJson(in_u, &out));
+  EXPECT_EQ("34", out);
+  EXPECT_TRUE(GetStringFromJson(in_b, &out));
+  EXPECT_EQ("true", out);
+  // Not supported here yet.
+  EXPECT_FALSE(GetStringFromJson(bad_a, &out));
+  EXPECT_FALSE(GetStringFromJson(bad_o, &out));
+}
+
+TEST(JsonTest, GetInt) {
+  int out;
+  EXPECT_TRUE(GetIntFromJson(in_sn, &out));
+  EXPECT_EQ(99, out);
+  EXPECT_TRUE(GetIntFromJson(in_si, &out));
+  EXPECT_EQ(-99, out);
+  EXPECT_TRUE(GetIntFromJson(in_n, &out));
+  EXPECT_EQ(12, out);
+  EXPECT_TRUE(GetIntFromJson(in_i, &out));
+  EXPECT_EQ(-12, out);
+  EXPECT_TRUE(GetIntFromJson(in_u, &out));
+  EXPECT_EQ(34, out);
+  EXPECT_TRUE(GetIntFromJson(in_b, &out));
+  EXPECT_EQ(1, out);
+  EXPECT_FALSE(GetIntFromJson(in_s, &out));
+  EXPECT_FALSE(GetIntFromJson(big_sn, &out));
+  EXPECT_FALSE(GetIntFromJson(big_si, &out));
+  EXPECT_FALSE(GetIntFromJson(big_u, &out));
+  EXPECT_FALSE(GetIntFromJson(bad_a, &out));
+  EXPECT_FALSE(GetIntFromJson(bad_o, &out));
+}
+
+TEST(JsonTest, GetUInt) {
+  unsigned int out;
+  EXPECT_TRUE(GetUIntFromJson(in_sn, &out));
+  EXPECT_EQ(99U, out);
+  EXPECT_TRUE(GetUIntFromJson(in_n, &out));
+  EXPECT_EQ(12U, out);
+  EXPECT_TRUE(GetUIntFromJson(in_u, &out));
+  EXPECT_EQ(34U, out);
+  EXPECT_TRUE(GetUIntFromJson(in_b, &out));
+  EXPECT_EQ(1U, out);
+  EXPECT_TRUE(GetUIntFromJson(big_u, &out));
+  EXPECT_EQ(0xFFFFFFFFU, out);
+  EXPECT_FALSE(GetUIntFromJson(in_s, &out));
+  // TODO: Fail reading negative strings.
+  // EXPECT_FALSE(GetUIntFromJson(in_si, &out));
+  EXPECT_FALSE(GetUIntFromJson(in_i, &out));
+  EXPECT_FALSE(GetUIntFromJson(big_sn, &out));
+  EXPECT_FALSE(GetUIntFromJson(big_si, &out));
+  EXPECT_FALSE(GetUIntFromJson(bad_a, &out));
+  EXPECT_FALSE(GetUIntFromJson(bad_o, &out));
+}
+
+TEST(JsonTest, GetBool) {
+  bool out;
+  EXPECT_TRUE(GetBoolFromJson(in_sb, &out));
+  EXPECT_EQ(true, out);
+  EXPECT_TRUE(GetBoolFromJson(in_n, &out));
+  EXPECT_EQ(true, out);
+  EXPECT_TRUE(GetBoolFromJson(in_i, &out));
+  EXPECT_EQ(true, out);
+  EXPECT_TRUE(GetBoolFromJson(in_u, &out));
+  EXPECT_EQ(true, out);
+  EXPECT_TRUE(GetBoolFromJson(in_b, &out));
+  EXPECT_EQ(true, out);
+  EXPECT_TRUE(GetBoolFromJson(big_u, &out));
+  EXPECT_EQ(true, out);
+  EXPECT_FALSE(GetBoolFromJson(in_s, &out));
+  EXPECT_FALSE(GetBoolFromJson(in_sn, &out));
+  EXPECT_FALSE(GetBoolFromJson(in_si, &out));
+  EXPECT_FALSE(GetBoolFromJson(big_sn, &out));
+  EXPECT_FALSE(GetBoolFromJson(big_si, &out));
+  EXPECT_FALSE(GetBoolFromJson(bad_a, &out));
+  EXPECT_FALSE(GetBoolFromJson(bad_o, &out));
+}
+
+TEST(JsonTest, GetDouble) {
+  double out;
+  EXPECT_TRUE(GetDoubleFromJson(in_sn, &out));
+  EXPECT_EQ(99, out);
+  EXPECT_TRUE(GetDoubleFromJson(in_si, &out));
+  EXPECT_EQ(-99, out);
+  EXPECT_TRUE(GetDoubleFromJson(in_sd, &out));
+  EXPECT_EQ(1.2, out);
+  EXPECT_TRUE(GetDoubleFromJson(in_n, &out));
+  EXPECT_EQ(12, out);
+  EXPECT_TRUE(GetDoubleFromJson(in_i, &out));
+  EXPECT_EQ(-12, out);
+  EXPECT_TRUE(GetDoubleFromJson(in_u, &out));
+  EXPECT_EQ(34, out);
+  EXPECT_TRUE(GetDoubleFromJson(in_b, &out));
+  EXPECT_EQ(1, out);
+  EXPECT_TRUE(GetDoubleFromJson(in_d, &out));
+  EXPECT_EQ(1.2, out);
+  EXPECT_FALSE(GetDoubleFromJson(in_s, &out));
+}
+
+TEST(JsonTest, GetFromArray) {
+  Json::Value a, out;
+  a.append(in_s);
+  a.append(in_i);
+  a.append(in_u);
+  a.append(in_b);
+  EXPECT_TRUE(GetValueFromJsonArray(a, 0, &out));
+  EXPECT_TRUE(GetValueFromJsonArray(a, 3, &out));
+  EXPECT_FALSE(GetValueFromJsonArray(a, 99, &out));
+  EXPECT_FALSE(GetValueFromJsonArray(a, 0xFFFFFFFF, &out));
+}
+
+TEST(JsonTest, GetFromObject) {
+  Json::Value o, out;
+  o["string"] = in_s;
+  o["int"] = in_i;
+  o["uint"] = in_u;
+  o["bool"] = in_b;
+  EXPECT_TRUE(GetValueFromJsonObject(o, "int", &out));
+  EXPECT_TRUE(GetValueFromJsonObject(o, "bool", &out));
+  EXPECT_FALSE(GetValueFromJsonObject(o, "foo", &out));
+  EXPECT_FALSE(GetValueFromJsonObject(o, "", &out));
+}
+
+TEST(JsonTest, VectorToArray) {
+  std::vector<std::string> in;
+  Json::Value out;
+  in.push_back("a");
+  in.push_back("b");
+  in.push_back("c");
+  out = StringVectorToJsonValue(in);
+  EXPECT_EQ(in.size(), out.size());
+  for (Json::Value::ArrayIndex i = 0; i < in.size(); ++i) {
+    EXPECT_EQ(in[i], out[i].asString());
+  }
+}
+
+TEST(JsonTest, ArrayToVector) {
+  Json::Value in(Json::arrayValue);
+  std::vector<std::string> out;
+  in.append("a");
+  in.append("b");
+  in.append("c");
+  EXPECT_TRUE(JsonValueToStringVector(in, &out));
+  EXPECT_EQ(in.size(), out.size());
+  for (Json::Value::ArrayIndex i = 0; i < in.size(); ++i) {
+    EXPECT_EQ(in[i].asString(), out[i]);
+  }
+}
diff --git a/talk/base/latebindingsymboltable_unittest.cc b/talk/base/latebindingsymboltable_unittest.cc
new file mode 100644
index 0000000..a16423e
--- /dev/null
+++ b/talk/base/latebindingsymboltable_unittest.cc
@@ -0,0 +1,80 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#ifdef LINUX
+#include <math.h>
+#endif
+
+#include "talk/base/gunit.h"
+#include "talk/base/latebindingsymboltable.h"
+
+namespace talk_base {
+
+#ifdef LINUX
+
+// Some libm symbols for our test.
+#define LIBM_SYMBOLS_LIST \
+  X(acos) \
+  X(sin) \
+  X(tan)
+
+LATE_BINDING_SYMBOL_TABLE_DECLARE_BEGIN(LibmTestSymbolTable)
+#define X(sym) \
+    LATE_BINDING_SYMBOL_TABLE_DECLARE_ENTRY(LibmTestSymbolTable, sym)
+LIBM_SYMBOLS_LIST
+#undef X
+LATE_BINDING_SYMBOL_TABLE_DECLARE_END(LibmTestSymbolTable)
+
+LATE_BINDING_SYMBOL_TABLE_DEFINE_BEGIN(LibmTestSymbolTable, "libm.so.6")
+#define X(sym) \
+    LATE_BINDING_SYMBOL_TABLE_DEFINE_ENTRY(LibmTestSymbolTable, sym)
+LIBM_SYMBOLS_LIST
+#undef X
+LATE_BINDING_SYMBOL_TABLE_DEFINE_END(LibmTestSymbolTable)
+
+#define LATE(sym) \
+  LATESYM_GET(LibmTestSymbolTable, &table, sym)
+
+TEST(LateBindingSymbolTable, libm) {
+  LibmTestSymbolTable table;
+  EXPECT_FALSE(table.IsLoaded());
+  ASSERT_TRUE(table.Load());
+  EXPECT_TRUE(table.IsLoaded());
+  EXPECT_EQ(LATE(acos)(0.5), acos(0.5));
+  EXPECT_EQ(LATE(sin)(0.5), sin(0.5));
+  EXPECT_EQ(LATE(tan)(0.5), tan(0.5));
+  // It would be nice to check that the addresses are the same, but the nature
+  // of dynamic linking and relocation makes them actually be different.
+  table.Unload();
+  EXPECT_FALSE(table.IsLoaded());
+}
+
+#else
+#error Not implemented
+#endif
+
+}  // namespace talk_base
diff --git a/talk/base/libdbusglibsymboltable.cc b/talk/base/libdbusglibsymboltable.cc
new file mode 100644
index 0000000..3548a7c
--- /dev/null
+++ b/talk/base/libdbusglibsymboltable.cc
@@ -0,0 +1,41 @@
+/*
+ * libjingle
+ * Copyright 2004--2011, 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.
+ */
+
+#include "talk/base/libdbusglibsymboltable.h"
+
+namespace talk_base {
+
+LATE_BINDING_SYMBOL_TABLE_DEFINE_BEGIN(LibDBusGlibSymbolTable,
+                                       "libdbus-glib-1.so")
+#define X(sym) \
+    LATE_BINDING_SYMBOL_TABLE_DEFINE_ENTRY(LibDBusGlibSymbolTable, sym)
+LIBDBUS_GLIB_SYMBOLS_LIST
+#undef X
+LATE_BINDING_SYMBOL_TABLE_DEFINE_END(LibDBusGlibSymbolTable)
+
+}  // namespace talk_base
+
diff --git a/talk/base/libdbusglibsymboltable.h b/talk/base/libdbusglibsymboltable.h
new file mode 100644
index 0000000..9aad8a5
--- /dev/null
+++ b/talk/base/libdbusglibsymboltable.h
@@ -0,0 +1,85 @@
+/*
+ * libjingle
+ * Copyright 2004--2011, 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.
+ */
+
+#ifndef TALK_BASE_LIBDBUSGLIBSYMBOLTABLE_H_
+#define TALK_BASE_LIBDBUSGLIBSYMBOLTABLE_H_
+
+#include "talk/base/latebindingsymboltable.h"
+
+namespace talk_base {
+
+// The libdbus-glib symbols we need, as an X-Macro list.
+// This list must contain precisely every libdbus-glib function that is used in
+// dbus.cc.
+#define LIBDBUS_GLIB_SYMBOLS_LIST \
+  X(dbus_bus_add_match) \
+  X(dbus_connection_add_filter) \
+  X(dbus_connection_close) \
+  X(dbus_connection_remove_filter) \
+  X(dbus_connection_set_exit_on_disconnect) \
+  X(dbus_g_bus_get) \
+  X(dbus_g_bus_get_private) \
+  X(dbus_g_connection_get_connection) \
+  X(dbus_g_connection_unref) \
+  X(dbus_g_thread_init) \
+  X(dbus_message_get_interface) \
+  X(dbus_message_get_member) \
+  X(dbus_message_get_path) \
+  X(dbus_message_get_type) \
+  X(dbus_message_iter_get_arg_type) \
+  X(dbus_message_iter_get_basic) \
+  X(dbus_message_iter_init) \
+  X(dbus_message_ref) \
+  X(dbus_message_unref) \
+  X(g_free) \
+  X(g_idle_source_new) \
+  X(g_main_context_new) \
+  X(g_main_context_push_thread_default) \
+  X(g_main_context_unref) \
+  X(g_main_loop_new) \
+  X(g_main_loop_quit) \
+  X(g_main_loop_run) \
+  X(g_main_loop_unref) \
+  X(g_object_unref) \
+  X(g_source_attach) \
+  X(g_source_destroy) \
+  X(g_source_set_callback) \
+  X(g_source_unref) \
+  X(g_thread_init) \
+  X(g_type_init)
+
+LATE_BINDING_SYMBOL_TABLE_DECLARE_BEGIN(LibDBusGlibSymbolTable)
+#define X(sym) \
+    LATE_BINDING_SYMBOL_TABLE_DECLARE_ENTRY(LibDBusGlibSymbolTable, sym)
+LIBDBUS_GLIB_SYMBOLS_LIST
+#undef X
+LATE_BINDING_SYMBOL_TABLE_DECLARE_END(LibDBusGlibSymbolTable)
+
+}  // namespace talk_base
+
+#endif  // TALK_BASE_LIBDBUSGLIBSYMBOLTABLE_H_
+
diff --git a/talk/base/linux_unittest.cc b/talk/base/linux_unittest.cc
new file mode 100644
index 0000000..6e1a8a9
--- /dev/null
+++ b/talk/base/linux_unittest.cc
@@ -0,0 +1,119 @@
+/*
+ * libjingle
+ * Copyright 2008, 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.
+ */
+
+#include <stdio.h>
+#include "talk/base/linux.h"
+#include "talk/base/fileutils.h"
+#include "talk/base/logging.h"
+#include "testing/base/gunit.h"
+
+namespace talk_base {
+
+// These tests running on ARM are fairly specific to the output of the tegra2
+// ARM processor, and so may fail on other ARM-based systems.
+TEST(ProcCpuInfo, GetProcInfo) {
+  ProcCpuInfo proc_info;
+  EXPECT_TRUE(proc_info.LoadFromSystem());
+
+  int out_cpus = 0;
+  EXPECT_TRUE(proc_info.GetNumCpus(&out_cpus));
+  LOG(LS_INFO) << "GetNumCpus: " << out_cpus;
+  EXPECT_GT(out_cpus, 0);
+
+  int out_cpus_phys = 0;
+  EXPECT_TRUE(proc_info.GetNumPhysicalCpus(&out_cpus_phys));
+  LOG(LS_INFO) << "GetNumPhysicalCpus: " << out_cpus_phys;
+  EXPECT_GT(out_cpus_phys, 0);
+  EXPECT_LE(out_cpus_phys, out_cpus);
+
+  int out_family = 0;
+  EXPECT_TRUE(proc_info.GetCpuFamily(&out_family));
+  LOG(LS_INFO) << "cpu family: " << out_family;
+  EXPECT_GE(out_family, 4);
+
+#if defined(__arm__)
+  std::string out_processor;
+  EXPECT_TRUE(proc_info.GetSectionStringValue(0, "Processor", &out_processor));
+  LOG(LS_INFO) << "Processor: " << out_processor;
+  EXPECT_NE(std::string::npos, out_processor.find("ARM"));
+
+  // Most other info, such as model, stepping, vendor, etc.
+  // is missing on ARM systems.
+#else
+  int out_model = 0;
+  EXPECT_TRUE(proc_info.GetSectionIntValue(0, "model", &out_model));
+  LOG(LS_INFO) << "model: " << out_model;
+
+  int out_stepping = 0;
+  EXPECT_TRUE(proc_info.GetSectionIntValue(0, "stepping", &out_stepping));
+  LOG(LS_INFO) << "stepping: " << out_stepping;
+
+  int out_processor = 0;
+  EXPECT_TRUE(proc_info.GetSectionIntValue(0, "processor", &out_processor));
+  LOG(LS_INFO) << "processor: " << out_processor;
+  EXPECT_EQ(0, out_processor);
+
+  std::string out_str;
+  EXPECT_TRUE(proc_info.GetSectionStringValue(0, "vendor_id", &out_str));
+  LOG(LS_INFO) << "vendor_id: " << out_str;
+  EXPECT_FALSE(out_str.empty());
+#endif
+}
+
+TEST(ConfigParser, ParseConfig) {
+  ConfigParser parser;
+  MemoryStream *test_stream = new MemoryStream(
+      "Key1: Value1\n"
+      "Key2\t: Value2\n"
+      "Key3:Value3\n"
+      "\n"
+      "Key1:Value1\n");
+  ConfigParser::MapVector key_val_pairs;
+  parser.Attach(test_stream);
+  EXPECT_EQ(true, parser.Parse(&key_val_pairs));
+  EXPECT_EQ(2U, key_val_pairs.size());
+  EXPECT_EQ("Value1", key_val_pairs[0]["Key1"]);
+  EXPECT_EQ("Value2", key_val_pairs[0]["Key2"]);
+  EXPECT_EQ("Value3", key_val_pairs[0]["Key3"]);
+  EXPECT_EQ("Value1", key_val_pairs[1]["Key1"]);
+  key_val_pairs.clear();
+  EXPECT_EQ(true, parser.Open("/proc/cpuinfo"));
+  EXPECT_EQ(true, parser.Parse(&key_val_pairs));
+}
+
+TEST(ReadLinuxLsbRelease, ReturnsSomething) {
+  std::string str = ReadLinuxLsbRelease();
+  // ChromeOS don't have lsb_release
+  // EXPECT_FALSE(str.empty());
+}
+
+TEST(ReadLinuxUname, ReturnsSomething) {
+  std::string str = ReadLinuxUname();
+  EXPECT_FALSE(str.empty());
+}
+
+}  // namespace talk_base
diff --git a/talk/base/linuxfdwalk.h b/talk/base/linuxfdwalk.h
new file mode 100644
index 0000000..ea039bf
--- /dev/null
+++ b/talk/base/linuxfdwalk.h
@@ -0,0 +1,51 @@
+/*
+ * libjingle
+ * Copyright 2004--2009, 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.
+ */
+
+#ifndef TALK_BASE_LINUXFDWALK_H_
+#define TALK_BASE_LINUXFDWALK_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Linux port of SunOS's fdwalk(3) call. It loops over all open file descriptors
+// and calls func on each one. Additionally, it is safe to use from the child
+// of a fork that hasn't exec'ed yet, so you can use it to close all open file
+// descriptors prior to exec'ing a daemon.
+// The return value is 0 if successful, or else -1 and errno is set. The
+// possible errors include any error that can be returned by opendir(),
+// readdir(), or closedir(), plus EBADF if there are problems parsing the
+// contents of /proc/self/fd.
+// The file descriptors that are enumerated will not include the file descriptor
+// used for the enumeration itself.
+int fdwalk(void (*func)(void *, int), void *opaque);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // TALK_BASE_LINUXFDWALK_H_
diff --git a/talk/base/maccocoasocketserver.h b/talk/base/maccocoasocketserver.h
new file mode 100644
index 0000000..41c9f71
--- /dev/null
+++ b/talk/base/maccocoasocketserver.h
@@ -0,0 +1,106 @@
+/*
+ * libjingle
+ * Copyright 2007, 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.
+ */
+
+// A libjingle compatible SocketServer for OSX/iOS/Cocoa.
+
+#if !defined(USE_COCOA_THREADING) || (!defined(OSX) && !defined(IOS))
+#error You can only include this file on OSX or IOS, and must also define
+#error USE_COCOA_THREADING.
+#endif
+
+#import <Foundation/Foundation.h>
+
+#import "talk/base/messagequeue.h"
+#import "talk/base/socketserver.h"
+
+namespace talk_base {
+class MacCocoaSocketServer;
+}
+
+// MacCocoaSocketServerHelper serves as a delegate to NSMachPort or a target for
+// a timeout.
+@interface MacCocoaSocketServerHelper : NSObject {
+  // This is a weak reference. This works fine since the
+  // talk_base::MacCocoaSocketServer owns this object.
+  talk_base::MacCocoaSocketServer* socketServer; // weak.
+}
+
+@end
+
+namespace talk_base {
+
+// The name "SocketServer" is misleading for this class. This class inherits
+// from SocketServer, some variants of which create/use physical
+// sockets (specifically, PhysicalSocketServer). A general explanation
+// (courtesy of bpm) is that SocketServer is responsible for allowing a thread
+// to go into an efficient wait state while it waits for a timer to expire or an
+// incoming message from MessageQueue.  For GUI applications, the main thread
+// must continue to pump the main event loop during this wait state, hence the
+// need for specialized SocketServers to run on the main thread.
+//
+// This particular socketServer however cannot handle a generalized wait state.
+// It can only handle Wait when called with cms == 0, or when cms > 0 and
+// process_io is false. In the former case, the process_io argument is ignored,
+// and in the latter case, the thread will sleep for cms milliSeconds.
+//
+// Use this class in a Cocoa application that uses libjingle's Task and
+// MessageQueue, on the main thread. The typical usage is something like this:
+//
+//  talk_base::Thread* current = talk_base::Thread::Current();
+//  MacCocoaSocketServer* ss =
+//      new talk_base::MacCocoaSocketServer(current);
+//  current->set_socketserver(ss);
+//
+//  // Now schedule some libjingle tasks, then call
+//
+//  [NSApp run];
+
+class MacCocoaSocketServer : public SocketServer {
+ public:
+  explicit MacCocoaSocketServer(MessageQueue* message_queue);
+  virtual ~MacCocoaSocketServer();
+
+  // SocketServer Interface. We don't create any synchronous sockets.
+  virtual Socket* CreateSocket(int type) { return NULL; }
+  virtual AsyncSocket* CreateAsyncSocket(int type);
+
+  virtual bool Wait(int cms, bool process_io);
+  virtual void WakeUp();
+
+  void Pump();
+
+ private:
+  // message_queue_ is not owned by this object.
+  // It is cached from the constructor argument.
+  MessageQueue* message_queue_; // weak.
+  MacCocoaSocketServerHelper* helper_;
+  NSTimer* timer_;
+
+  DISALLOW_EVIL_CONSTRUCTORS(MacCocoaSocketServer);
+};
+
+}  // namespace talk_base
diff --git a/talk/base/maccocoasocketserver.mm b/talk/base/maccocoasocketserver.mm
new file mode 100644
index 0000000..6a91d43
--- /dev/null
+++ b/talk/base/maccocoasocketserver.mm
@@ -0,0 +1,127 @@
+/*
+ * libjingle
+ * Copyright 2007, 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.
+ */
+
+#import <assert.h>
+
+#import "talk/base/maccocoasocketserver.h"
+#import "talk/base/logging.h"
+#import "talk/base/macasyncsocket.h"
+
+static const double kTimerIntervalSecs = 0.1;
+
+@implementation MacCocoaSocketServerHelper
+
+- (id)initWithSocketServer:(talk_base::MacCocoaSocketServer*)ss {
+  self = [super init];
+  if (self) {
+    socketServer = ss;
+  }
+  return self;
+}
+
+- (void)timerFired:(NSTimer*)timer {
+  socketServer->Pump();
+}
+
+- (void)wakeUp {
+  socketServer->Pump();
+}
+
+@end
+
+namespace talk_base {
+
+MacCocoaSocketServer::MacCocoaSocketServer(MessageQueue* message_queue) :
+    message_queue_(message_queue) {
+  helper_ = [[MacCocoaSocketServerHelper alloc] initWithSocketServer:this];
+
+  NSTimer* timer =
+      [NSTimer scheduledTimerWithTimeInterval:kTimerIntervalSecs
+                                       target:helper_
+                                     selector:@selector(timerFired:)
+                                     userInfo:nil
+                                      repeats:YES];
+  timer_ = [timer retain];
+}
+
+MacCocoaSocketServer::~MacCocoaSocketServer() {
+  [timer_ invalidate];
+  [timer_ release];
+  [helper_ release];
+}
+
+AsyncSocket* MacCocoaSocketServer::CreateAsyncSocket(int type) {
+  assert(type == SOCK_STREAM);
+  return new MacAsyncSocket();
+}
+
+bool MacCocoaSocketServer::Wait(int cms, bool process_io) {
+  // Ideally we would have some way to run the UI loop for |cms| milliSeconds,
+  // or until WakeUp() is called (whichever is earlier).
+  // But there is no good solution for that - stopping/restarting the
+  // NSApp run loop or calling nextEventMatchingTask both have significant
+  // overhead, resulting in high CPU utilization when there are a lot of
+  // libjingle messages (hence WakeUp and Wait calls) floating around.
+
+  // Simply calling "usleep" will block the UI, which is OK when |process_io|
+  // is false.
+
+  if (cms != 0) {
+    assert(cms > 0 && !process_io);
+    if (cms < 0 || process_io) {
+      return false;
+    }
+    usleep(cms * 1000);
+  }
+
+  return true;
+}
+
+void MacCocoaSocketServer::WakeUp() {
+  [helper_ performSelectorOnMainThread:@selector(wakeUp)
+                            withObject:nil
+                         waitUntilDone:NO];
+}
+
+void MacCocoaSocketServer::Pump() {
+  // Process messages.
+
+  Message msg;
+  // We don't want to process an unbounded number of messages - while we do that
+  // the UI remains blocked. So we only process as many messages as are in the
+  // queue when we start.
+  //
+  // max(1,..) ensures we run Get() at least once, this is needed to check
+  // for "sent" messages that otherwise are not included into size() result.
+  for (size_t max_messages_to_process = _max<size_t>(1, message_queue_->size());
+       max_messages_to_process > 0 && message_queue_->Get(&msg, 0);
+       --max_messages_to_process) {
+    message_queue_->Dispatch(&msg);
+  }
+}
+
+}  // namespace talk_base
diff --git a/talk/base/maccocoathreadhelper.mm b/talk/base/maccocoathreadhelper.mm
new file mode 100644
index 0000000..e8482ce
--- /dev/null
+++ b/talk/base/maccocoathreadhelper.mm
@@ -0,0 +1,78 @@
+/*
+ * libjingle
+ * Copyright 2007, 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.
+ */
+// Helper function for using Cocoa with Posix threading.
+
+#import <assert.h>
+#import <Foundation/Foundation.h>
+
+#import "talk/base/maccocoathreadhelper.h"
+
+// A dummy thread object which essentially does nothing.
+// This is used in InitCocoaMultiThreading method.
+@interface DummyThreadObject : NSObject {
+}
+
++ (void)dummyFunc:(NSObject*)obj;
+
+@end
+
+@implementation DummyThreadObject
+
++ (void)dummyFunc:(NSObject*)obj {
+  // nothing to do
+}
+
+@end
+
+namespace talk_base {
+
+// Cocoa must be "put into multithreading mode" before Cocoa functionality can
+// be used on POSIX threads. The way to do that is to spawn one thread that may
+// immediately exit.
+void InitCocoaMultiThreading() {
+  if ([NSThread isMultiThreaded] == NO) {
+    // The sole purpose of this autorelease pool is to avoid a console
+    // message on Leopard that tells us we're autoreleasing the thread
+    // with no autorelease pool in place; we can't set up an autorelease
+    // pool before this, because this is executed from an initializer,
+    // which is run before main.  This means we leak an autorelease pool,
+    // and one thread, and if other objects are set up in initializers after
+    // this they'll be silently added to this pool and never released.
+
+    // Doing NSAutoreleasePool* hack = [[NSAutoreleasePool alloc] init];
+    // causes unused variable error.
+    NSAutoreleasePool* hack;
+    hack = [[NSAutoreleasePool alloc] init];
+    [NSThread detachNewThreadSelector:@selector(dummyFunc:)
+                             toTarget:[DummyThreadObject class]
+                           withObject:nil];
+  }
+
+  assert([NSThread isMultiThreaded]);
+}
+
+}  // namespace talk_base
diff --git a/talk/base/macsocketserver_unittest.cc b/talk/base/macsocketserver_unittest.cc
new file mode 100644
index 0000000..a204c24
--- /dev/null
+++ b/talk/base/macsocketserver_unittest.cc
@@ -0,0 +1,195 @@
+/*
+ * libjingle
+ * Copyright 2009, 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.
+ */
+
+#include "talk/base/gunit.h"
+#include "talk/base/scoped_ptr.h"
+#include "talk/base/socket_unittest.h"
+#include "talk/base/thread.h"
+#include "talk/base/macsocketserver.h"
+
+namespace talk_base {
+
+class WakeThread : public Thread {
+ public:
+  WakeThread(SocketServer* ss) : ss_(ss) {
+  }
+  void Run() {
+    ss_->WakeUp();
+  }
+ private:
+  SocketServer* ss_;
+};
+
+// Test that MacCFSocketServer::Wait works as expected.
+TEST(MacCFSocketServerTest, TestWait) {
+  MacCFSocketServer server;
+  uint32 start = Time();
+  server.Wait(1000, true);
+  EXPECT_GE(TimeSince(start), 1000);
+}
+
+// Test that MacCFSocketServer::Wakeup works as expected.
+TEST(MacCFSocketServerTest, TestWakeup) {
+  MacCFSocketServer server;
+  WakeThread thread(&server);
+  uint32 start = Time();
+  thread.Start();
+  server.Wait(10000, true);
+  EXPECT_LT(TimeSince(start), 10000);
+}
+
+// Test that MacCarbonSocketServer::Wait works as expected.
+TEST(MacCarbonSocketServerTest, TestWait) {
+  MacCarbonSocketServer server;
+  uint32 start = Time();
+  server.Wait(1000, true);
+  EXPECT_GE(TimeSince(start), 1000);
+}
+
+// Test that MacCarbonSocketServer::Wakeup works as expected.
+TEST(MacCarbonSocketServerTest, TestWakeup) {
+  MacCarbonSocketServer server;
+  WakeThread thread(&server);
+  uint32 start = Time();
+  thread.Start();
+  server.Wait(10000, true);
+  EXPECT_LT(TimeSince(start), 10000);
+}
+
+// Test that MacCarbonAppSocketServer::Wait works as expected.
+TEST(MacCarbonAppSocketServerTest, TestWait) {
+  MacCarbonAppSocketServer server;
+  uint32 start = Time();
+  server.Wait(1000, true);
+  EXPECT_GE(TimeSince(start), 1000);
+}
+
+// Test that MacCarbonAppSocketServer::Wakeup works as expected.
+TEST(MacCarbonAppSocketServerTest, TestWakeup) {
+  MacCarbonAppSocketServer server;
+  WakeThread thread(&server);
+  uint32 start = Time();
+  thread.Start();
+  server.Wait(10000, true);
+  EXPECT_LT(TimeSince(start), 10000);
+}
+
+// Test that MacAsyncSocket passes all the generic Socket tests.
+class MacAsyncSocketTest : public SocketTest {
+ protected:
+  MacAsyncSocketTest()
+      : server_(CreateSocketServer()),
+        scope_(server_.get()) {}
+  // Override for other implementations of MacBaseSocketServer.
+  virtual MacBaseSocketServer* CreateSocketServer() {
+    return new MacCFSocketServer();
+  };
+  talk_base::scoped_ptr<MacBaseSocketServer> server_;
+  SocketServerScope scope_;
+};
+
+TEST_F(MacAsyncSocketTest, TestConnect) {
+  SocketTest::TestConnect();
+}
+
+TEST_F(MacAsyncSocketTest, TestConnectWithDnsLookup) {
+  SocketTest::TestConnectWithDnsLookup();
+}
+
+TEST_F(MacAsyncSocketTest, TestConnectFail) {
+  SocketTest::TestConnectFail();
+}
+
+// Reenable once we have mac async dns
+TEST_F(MacAsyncSocketTest, DISABLED_TestConnectWithDnsLookupFail) {
+  SocketTest::TestConnectWithDnsLookupFail();
+}
+
+TEST_F(MacAsyncSocketTest, TestConnectWithClosedSocket) {
+  SocketTest::TestConnectWithClosedSocket();
+}
+
+// Flaky at the moment (10% failure rate).  Seems the client doesn't get
+// signalled in a timely manner...
+TEST_F(MacAsyncSocketTest, DISABLED_TestServerCloseDuringConnect) {
+  SocketTest::TestServerCloseDuringConnect();
+}
+// Flaky at the moment (0.5% failure rate).  Seems the client doesn't get
+// signalled in a timely manner...
+TEST_F(MacAsyncSocketTest, TestClientCloseDuringConnect) {
+  SocketTest::TestClientCloseDuringConnect();
+}
+
+TEST_F(MacAsyncSocketTest, TestServerClose) {
+  SocketTest::TestServerClose();
+}
+
+TEST_F(MacAsyncSocketTest, TestCloseInClosedCallback) {
+  SocketTest::TestCloseInClosedCallback();
+}
+
+TEST_F(MacAsyncSocketTest, TestSocketServerWait) {
+  SocketTest::TestSocketServerWait();
+}
+
+TEST_F(MacAsyncSocketTest, TestTcp) {
+  SocketTest::TestTcp();
+}
+
+TEST_F(MacAsyncSocketTest, TestSingleFlowControlCallback) {
+  SocketTest::TestSingleFlowControlCallback();
+}
+
+TEST_F(MacAsyncSocketTest, DISABLED_TestUdp) {
+  SocketTest::TestUdp();
+}
+
+TEST_F(MacAsyncSocketTest, DISABLED_TestGetSetOptions) {
+  SocketTest::TestGetSetOptions();
+}
+
+class MacCarbonAsyncSocketTest : public MacAsyncSocketTest {
+  virtual MacBaseSocketServer* CreateSocketServer() {
+    return new MacCarbonSocketServer();
+  };
+};
+
+TEST_F(MacCarbonAsyncSocketTest, TestSocketServerWait) {
+  SocketTest::TestSocketServerWait();
+}
+
+class MacCarbonAppAsyncSocketTest : public MacAsyncSocketTest {
+  virtual MacBaseSocketServer* CreateSocketServer() {
+    return new MacCarbonAppSocketServer();
+  };
+};
+
+TEST_F(MacCarbonAppAsyncSocketTest, TestSocketServerWait) {
+  SocketTest::TestSocketServerWait();
+}
+
+}  // namespace talk_base
diff --git a/talk/base/macutils_unittest.cc b/talk/base/macutils_unittest.cc
new file mode 100644
index 0000000..c66000a
--- /dev/null
+++ b/talk/base/macutils_unittest.cc
@@ -0,0 +1,54 @@
+/*
+ * libjingle
+ * Copyright 2009, 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.
+ */
+
+#include "talk/base/gunit.h"
+#include "talk/base/macutils.h"
+
+TEST(MacUtilsTest, GetOsVersionName) {
+  talk_base::MacOSVersionName ver = talk_base::GetOSVersionName();
+  EXPECT_NE(talk_base::kMacOSUnknown, ver);
+}
+
+TEST(MacUtilsTest, GetQuickTimeVersion) {
+  std::string version;
+  EXPECT_TRUE(talk_base::GetQuickTimeVersion(&version));
+}
+
+TEST(MacUtilsTest, RunAppleScriptCompileError) {
+  std::string script("set value to to 5");
+  EXPECT_FALSE(talk_base::RunAppleScript(script));
+}
+
+TEST(MacUtilsTest, RunAppleScriptRuntimeError) {
+  std::string script("set value to 5 / 0");
+  EXPECT_FALSE(talk_base::RunAppleScript(script));
+}
+
+TEST(MacUtilsTest, RunAppleScriptSuccess) {
+  std::string script("set value to 5");
+  EXPECT_TRUE(talk_base::RunAppleScript(script));
+}
diff --git a/talk/base/macwindowpicker.cc b/talk/base/macwindowpicker.cc
new file mode 100644
index 0000000..147e8e9
--- /dev/null
+++ b/talk/base/macwindowpicker.cc
@@ -0,0 +1,220 @@
+// Copyright 2010 Google Inc. All Rights Reserved
+
+
+#include "talk/base/macwindowpicker.h"
+
+#include <ApplicationServices/ApplicationServices.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <dlfcn.h>
+
+#include "talk/base/logging.h"
+#include "talk/base/macutils.h"
+
+namespace talk_base {
+
+static const char* kCoreGraphicsName =
+    "/System/Library/Frameworks/ApplicationServices.framework/Frameworks/"
+    "CoreGraphics.framework/CoreGraphics";
+
+static const char* kWindowListCopyWindowInfo = "CGWindowListCopyWindowInfo";
+static const char* kWindowListCreateDescriptionFromArray =
+    "CGWindowListCreateDescriptionFromArray";
+
+// Function pointer for holding the CGWindowListCopyWindowInfo function.
+typedef CFArrayRef(*CGWindowListCopyWindowInfoProc)(CGWindowListOption,
+                                                    CGWindowID);
+
+// Function pointer for holding the CGWindowListCreateDescriptionFromArray
+// function.
+typedef CFArrayRef(*CGWindowListCreateDescriptionFromArrayProc)(CFArrayRef);
+
+MacWindowPicker::MacWindowPicker() : lib_handle_(NULL), get_window_list_(NULL),
+                                     get_window_list_desc_(NULL) {
+}
+
+MacWindowPicker::~MacWindowPicker() {
+  if (lib_handle_ != NULL) {
+    dlclose(lib_handle_);
+  }
+}
+
+bool MacWindowPicker::Init() {
+  // TODO: If this class grows to use more dynamically functions
+  // from the CoreGraphics framework, consider using
+  // talk/base/latebindingsymboltable.h.
+  lib_handle_ = dlopen(kCoreGraphicsName, RTLD_NOW);
+  if (lib_handle_ == NULL) {
+    LOG(LS_ERROR) << "Could not load CoreGraphics";
+    return false;
+  }
+
+  get_window_list_ = dlsym(lib_handle_, kWindowListCopyWindowInfo);
+  get_window_list_desc_ =
+      dlsym(lib_handle_, kWindowListCreateDescriptionFromArray);
+  if (get_window_list_ == NULL || get_window_list_desc_ == NULL) {
+    // The CGWindowListCopyWindowInfo and the
+    // CGWindowListCreateDescriptionFromArray functions was introduced
+    // in Leopard(10.5) so this is a normal failure on Tiger.
+    LOG(LS_INFO) << "Failed to load Core Graphics symbols";
+    dlclose(lib_handle_);
+    lib_handle_ = NULL;
+    return false;
+  }
+
+  return true;
+}
+
+bool MacWindowPicker::IsVisible(WindowId id) {
+  // Init if we're not already inited.
+  if (get_window_list_desc_ == NULL && !Init()) {
+    return false;
+  }
+  CGWindowID ids[1];
+  ids[0] = id;
+  CFArrayRef window_id_array =
+      CFArrayCreate(NULL, reinterpret_cast<const void **>(&ids), 1, NULL);
+
+  CFArrayRef window_array =
+      reinterpret_cast<CGWindowListCreateDescriptionFromArrayProc>(
+          get_window_list_desc_)(window_id_array);
+  if (window_array == NULL || 0 == CFArrayGetCount(window_array)) {
+    // Could not find the window. It might have been closed.
+    LOG(LS_INFO) << "Window not found";
+    CFRelease(window_id_array);
+    return false;
+  }
+
+  CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(
+      CFArrayGetValueAtIndex(window_array, 0));
+  CFBooleanRef is_visible = reinterpret_cast<CFBooleanRef>(
+      CFDictionaryGetValue(window, kCGWindowIsOnscreen));
+
+  // Check that the window is visible. If not we might crash.
+  bool visible = false;
+  if (is_visible != NULL) {
+    visible = CFBooleanGetValue(is_visible);
+  }
+  CFRelease(window_id_array);
+  CFRelease(window_array);
+  return visible;
+}
+
+bool MacWindowPicker::MoveToFront(WindowId id) {
+  // TODO: Implement this method.
+  // Init if we're not already inited.
+  if (get_window_list_desc_ == NULL && !Init()) {
+    return false;
+  }
+  CGWindowID ids[1];
+  ids[0] = id;
+  CFArrayRef window_id_array =
+      CFArrayCreate(NULL, reinterpret_cast<const void **>(&ids), 1, NULL);
+
+  CFArrayRef window_array =
+      reinterpret_cast<CGWindowListCreateDescriptionFromArrayProc>(
+          get_window_list_desc_)(window_id_array);
+  if (window_array == NULL || 0 == CFArrayGetCount(window_array)) {
+    // Could not find the window. It might have been closed.
+    LOG(LS_INFO) << "Window not found";
+    CFRelease(window_id_array);
+    return false;
+  }
+
+  CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(
+      CFArrayGetValueAtIndex(window_array, 0));
+  CFStringRef window_name_ref = reinterpret_cast<CFStringRef>(
+      CFDictionaryGetValue(window, kCGWindowName));
+  CFNumberRef application_pid = reinterpret_cast<CFNumberRef>(
+      CFDictionaryGetValue(window, kCGWindowOwnerPID));
+
+  int pid_val;
+  CFNumberGetValue(application_pid, kCFNumberIntType, &pid_val);
+  std::string window_name;
+  ToUtf8(window_name_ref, &window_name);
+
+  // Build an applescript that sets the selected window to front
+  // within the application. Then set the application to front.
+  bool result = true;
+  std::stringstream ss;
+  ss << "tell application \"System Events\"\n"
+     << "set proc to the first item of (every process whose unix id is "
+     << pid_val
+     << ")\n"
+     << "tell proc to perform action \"AXRaise\" of window \""
+     << window_name
+     << "\"\n"
+     << "set the frontmost of proc to true\n"
+     << "end tell";
+  if (!RunAppleScript(ss.str())) {
+    // This might happen to for example X applications where the X
+    // server spawns of processes with their own PID but the X server
+    // is still registered as owner to the application windows. As a
+    // workaround, we put the X server process to front, meaning that
+    // all X applications will show up. The drawback with this
+    // workaround is that the application that we really wanted to set
+    // to front might be behind another X application.
+    ProcessSerialNumber psn;
+    pid_t pid = pid_val;
+    int res = GetProcessForPID(pid, &psn);
+    if (res != 0) {
+      LOG(LS_ERROR) << "Failed getting process for pid";
+      result = false;
+    }
+    res = SetFrontProcess(&psn);
+    if (res != 0) {
+      LOG(LS_ERROR) << "Failed setting process to front";
+      result = false;
+    }
+  }
+  CFRelease(window_id_array);
+  CFRelease(window_array);
+  return result;
+}
+
+bool MacWindowPicker::GetWindowList(WindowDescriptionList* descriptions) {
+  // Init if we're not already inited.
+  if (get_window_list_ == NULL && !Init()) {
+    return false;
+  }
+
+  // Only get onscreen, non-desktop windows.
+  CFArrayRef window_array =
+      reinterpret_cast<CGWindowListCopyWindowInfoProc>(get_window_list_)(
+          kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
+          kCGNullWindowID);
+  if (window_array == NULL) {
+    return false;
+  }
+
+  // Check windows to make sure they have an id, title, and use window layer 0.
+  CFIndex i;
+  CFIndex count = CFArrayGetCount(window_array);
+  for (i = 0; i < count; ++i) {
+    CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(
+        CFArrayGetValueAtIndex(window_array, i));
+    CFStringRef window_title = reinterpret_cast<CFStringRef>(
+        CFDictionaryGetValue(window, kCGWindowName));
+    CFNumberRef window_id = reinterpret_cast<CFNumberRef>(
+        CFDictionaryGetValue(window, kCGWindowNumber));
+    CFNumberRef window_layer = reinterpret_cast<CFNumberRef>(
+        CFDictionaryGetValue(window, kCGWindowLayer));
+    if (window_title != NULL && window_id != NULL && window_layer != NULL) {
+      std::string title_str;
+      int id_val, layer_val;
+      ToUtf8(window_title, &title_str);
+      CFNumberGetValue(window_id, kCFNumberIntType, &id_val);
+      CFNumberGetValue(window_layer, kCFNumberIntType, &layer_val);
+
+      // Discard windows without a title.
+      if (layer_val == 0 && title_str.length() > 0) {
+        WindowDescription desc(id_val, title_str);
+        descriptions->push_back(desc);
+      }
+    }
+  }
+
+  CFRelease(window_array);
+  return true;
+}
+
+}  // namespace talk_base
diff --git a/talk/base/macwindowpicker.h b/talk/base/macwindowpicker.h
new file mode 100644
index 0000000..8fff8a0
--- /dev/null
+++ b/talk/base/macwindowpicker.h
@@ -0,0 +1,28 @@
+// Copyright 2010 Google Inc. All Rights Reserved
+
+
+#ifndef TALK_BASE_MACWINDOWPICKER_H_
+#define TALK_BASE_MACWINDOWPICKER_H_
+
+#include "talk/base/windowpicker.h"
+
+namespace talk_base {
+
+class MacWindowPicker : public WindowPicker {
+ public:
+  MacWindowPicker();
+  ~MacWindowPicker();
+  virtual bool Init();
+  virtual bool IsVisible(WindowId id);
+  virtual bool MoveToFront(WindowId id);
+  virtual bool GetWindowList(WindowDescriptionList* descriptions);
+
+ private:
+  void* lib_handle_;
+  void* get_window_list_;
+  void* get_window_list_desc_;
+};
+
+}  // namespace talk_base
+
+#endif  // TALK_BASE_MACWINDOWPICKER_H_
diff --git a/talk/base/macwindowpicker_unittest.cc b/talk/base/macwindowpicker_unittest.cc
new file mode 100644
index 0000000..9cb67db
--- /dev/null
+++ b/talk/base/macwindowpicker_unittest.cc
@@ -0,0 +1,39 @@
+// Copyright 2010 Google Inc. All Rights Reserved
+
+
+#include "talk/base/gunit.h"
+#include "talk/base/logging.h"
+#include "talk/base/macutils.h"
+#include "talk/base/macwindowpicker.h"
+#include "talk/base/windowpicker.h"
+
+#ifndef OSX
+#error Only for Mac OSX
+#endif
+
+namespace talk_base {
+
+bool IsLeopardOrLater() {
+  return GetOSVersionName() >= kMacOSLeopard;
+}
+
+// Test that this works on new versions and fails acceptably on old versions.
+TEST(MacWindowPickerTest, TestGetWindowList) {
+  MacWindowPicker picker, picker2;
+  WindowDescriptionList descriptions;
+  if (IsLeopardOrLater()) {
+    EXPECT_TRUE(picker.Init());
+    EXPECT_TRUE(picker.GetWindowList(&descriptions));
+    EXPECT_TRUE(picker2.GetWindowList(&descriptions));  // Init is optional
+  } else {
+    EXPECT_FALSE(picker.Init());
+    EXPECT_FALSE(picker.GetWindowList(&descriptions));
+    EXPECT_FALSE(picker2.GetWindowList(&descriptions));
+  }
+}
+
+// TODO: Add verification of the actual parsing, ie, add
+// functionality to inject a fake get_window_array function which
+// provide a pre-constructed list of windows.
+
+}  // namespace talk_base
diff --git a/talk/base/mathutils.h b/talk/base/mathutils.h
new file mode 100644
index 0000000..eeb110a
--- /dev/null
+++ b/talk/base/mathutils.h
@@ -0,0 +1,37 @@
+/* 
+ * libjingle
+ * Copyright 2005 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.
+ */
+
+#ifndef TALK_BASE_MATHUTILS_H_
+#define TALK_BASE_MATHUTILS_H_
+
+#include <math.h>
+
+#ifndef M_PI
+#define M_PI 3.14159265359f
+#endif
+
+#endif  // TALK_BASE_MATHUTILS_H_
diff --git a/talk/base/natserver.cc b/talk/base/natserver.cc
index 06493ad..7107f77 100644
--- a/talk/base/natserver.cc
+++ b/talk/base/natserver.cc
@@ -25,6 +25,7 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+#include "talk/base/natsocketfactory.h"
 #include "talk/base/natserver.h"
 #include "talk/base/logging.h"
 
@@ -60,7 +61,7 @@
 size_t AddrCmp::operator()(const SocketAddress& a) const {
   size_t h = 0;
   if (use_ip)
-    h ^= a.ip();
+    h ^= HashIP(a.ipaddr());
   if (use_port)
     h ^= a.port() | (a.port() << 16);
   return h;
@@ -68,9 +69,9 @@
 
 bool AddrCmp::operator()(
       const SocketAddress& a1, const SocketAddress& a2) const {
-  if (use_ip && (a1.ip() < a2.ip()))
+  if (use_ip && (a1.ipaddr() < a2.ipaddr()))
     return true;
-  if (use_ip && (a2.ip() < a1.ip()))
+  if (use_ip && (a2.ipaddr() < a1.ipaddr()))
     return false;
   if (use_port && (a1.port() < a2.port()))
     return true;
@@ -82,7 +83,7 @@
 NATServer::NATServer(
     NATType type, SocketFactory* internal, const SocketAddress& internal_addr,
     SocketFactory* external, const SocketAddress& external_ip)
-    : external_(external), external_ip_(external_ip.ip(), 0) {
+    : external_(external), external_ip_(external_ip.ipaddr(), 0) {
   nat_ = NAT::Create(type);
 
   server_socket_ = AsyncUDPSocket::Create(internal, internal_addr);
@@ -110,7 +111,7 @@
 
   // Read the intended destination from the wire.
   SocketAddress dest_addr;
-  dest_addr.Read_(buf, size);
+  int length = UnpackAddressFromNAT(buf, size, &dest_addr);
 
   // Find the translation for these addresses (allocating one if necessary).
   SocketAddressPair route(addr, dest_addr);
@@ -125,8 +126,7 @@
   iter->second->whitelist->insert(dest_addr);
 
   // Send the packet to its intended destination.
-  iter->second->socket->SendTo(
-      buf + dest_addr.Size_(), size - dest_addr.Size_(), dest_addr);
+  iter->second->socket->SendTo(buf + length, size - length, dest_addr);
 }
 
 void NATServer::OnExternalPacket(
@@ -147,16 +147,15 @@
   }
 
   // Forward this packet to the internal address.
-
-  size_t real_size = size + remote_addr.Size_();
-  char*  real_buf  = new char[real_size];
-
-  remote_addr.Write_(real_buf, real_size);
-  std::memcpy(real_buf + remote_addr.Size_(), buf, size);
-
-  server_socket_->SendTo(real_buf, real_size, iter->second->route.source());
-
-  delete[] real_buf;
+  // First prepend the address in a quasi-STUN format.
+  scoped_array<char> real_buf(new char[size + kNATEncodedIPv6AddressSize]);
+  size_t addrlength = PackAddressForNAT(real_buf.get(),
+                                        size + kNATEncodedIPv6AddressSize,
+                                        remote_addr);
+  // Copy the data part after the address.
+  std::memcpy(real_buf.get() + addrlength, buf, size);
+  server_socket_->SendTo(real_buf.get(), size + addrlength,
+                         iter->second->route.source());
 }
 
 void NATServer::Translate(const SocketAddressPair& route) {
diff --git a/talk/base/natserver_main.cc b/talk/base/natserver_main.cc
new file mode 100644
index 0000000..a748108
--- /dev/null
+++ b/talk/base/natserver_main.cc
@@ -0,0 +1,57 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#include <iostream>
+
+#include "talk/base/natserver.h"
+#include "talk/base/host.h"
+#include "talk/base/physicalsocketserver.h"
+
+using namespace talk_base;
+
+int main(int argc, char* argv[]) {
+  if (argc != 3) {
+    std::cerr << "usage: natserver <internal-ip> <external-ip>" << std::endl;
+    exit(1);
+  }
+
+  SocketAddress internal = SocketAddress(argv[1]);
+  SocketAddress external = SocketAddress(argv[2]);
+  if (internal.EqualIPs(external)) {
+    std::cerr << "internal and external IPs must differ" << std::endl;
+    exit(1);
+  }
+
+  Thread* pthMain = Thread::Current();
+  PhysicalSocketServer* ss = new PhysicalSocketServer();
+  pthMain->set_socketserver(ss);
+  NATServer* server = new NATServer(NAT_OPEN_CONE, ss, internal, ss, external);
+  server = server;
+
+  pthMain->Run();
+  return 0;
+}
diff --git a/talk/base/natsocketfactory.cc b/talk/base/natsocketfactory.cc
index 846d0e6..de2cf2b 100644
--- a/talk/base/natsocketfactory.cc
+++ b/talk/base/natsocketfactory.cc
@@ -33,6 +33,54 @@
 
 namespace talk_base {
 
+// Packs the given socketaddress into the buffer in buf, in the quasi-STUN
+// format that the natserver uses.
+// Returns 0 if an invalid address is passed.
+size_t PackAddressForNAT(char* buf, size_t buf_size,
+                         const SocketAddress& remote_addr) {
+  const IPAddress& ip = remote_addr.ipaddr();
+  int family = ip.family();
+  buf[0] = 0;
+  buf[1] = family;
+  // Writes the port.
+  *(reinterpret_cast<uint16*>(&buf[2])) = htons(remote_addr.port());
+  if (family == AF_INET) {
+    ASSERT(buf_size >= kNATEncodedIPv4AddressSize);
+    in_addr v4addr = ip.ipv4_address();
+    std::memcpy(&buf[4], &v4addr, kNATEncodedIPv4AddressSize - 4);
+    return kNATEncodedIPv4AddressSize;
+  } else if (family == AF_INET6) {
+    ASSERT(buf_size >= kNATEncodedIPv6AddressSize);
+    in6_addr v6addr = ip.ipv6_address();
+    std::memcpy(&buf[4], &v6addr, kNATEncodedIPv6AddressSize - 4);
+    return kNATEncodedIPv6AddressSize;
+  }
+  return 0U;
+}
+
+// Decodes the remote address from a packet that has been encoded with the nat's
+// quasi-STUN format. Returns the length of the address (i.e., the offset into
+// data where the original packet starts).
+size_t UnpackAddressFromNAT(const char* buf, size_t buf_size,
+                            SocketAddress* remote_addr) {
+  ASSERT(buf_size >= 8);
+  ASSERT(buf[0] == 0);
+  int family = buf[1];
+  uint16 port = ntohs(*(reinterpret_cast<const uint16*>(&buf[2])));
+  if (family == AF_INET) {
+    const in_addr* v4addr = reinterpret_cast<const in_addr*>(&buf[4]);
+    *remote_addr = SocketAddress(IPAddress(*v4addr), port);
+    return kNATEncodedIPv4AddressSize;
+  } else if (family == AF_INET6) {
+    ASSERT(buf_size >= 20);
+    const in6_addr* v6addr = reinterpret_cast<const in6_addr*>(&buf[4]);
+    *remote_addr = SocketAddress(IPAddress(*v6addr), port);
+    return kNATEncodedIPv6AddressSize;
+  }
+  return 0U;
+}
+
+
 // NATSocket
 class NATSocket : public AsyncSocket, public sigslot::has_slots<> {
  public:
@@ -95,43 +143,44 @@
     return result;
   }
 
-  virtual int Send(const void *pv, size_t cb) {
+  virtual int Send(const void* data, size_t size) {
     ASSERT(connected_);
-    return SendTo(pv, cb, remote_addr_);
+    return SendTo(data, size, remote_addr_);
   }
 
-  virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr) {
+  virtual int SendTo(const void* data, size_t size, const SocketAddress& addr) {
     ASSERT(!connected_ || addr == remote_addr_);
     if (server_addr_.IsAny() || type_ == SOCK_STREAM) {
-      return socket_->SendTo(pv, cb, addr);
+      return socket_->SendTo(data, size, addr);
     }
-
-    size_t size = cb + addr.Size_();
-    scoped_array<char> buf(new char[size]);
-    Encode(static_cast<const char*>(pv), cb, buf.get(), size, addr);
-
-    int result = socket_->SendTo(buf.get(), size, server_addr_);
+    // This array will be too large for IPv4 packets, but only by 12 bytes.
+    scoped_array<char> buf(new char[size + kNATEncodedIPv6AddressSize]);
+    size_t addrlength = PackAddressForNAT(buf.get(),
+                                          size + kNATEncodedIPv6AddressSize,
+                                          addr);
+    size_t encoded_size = size + addrlength;
+    std::memcpy(buf.get() + addrlength, data, size);
+    int result = socket_->SendTo(buf.get(), encoded_size, server_addr_);
     if (result >= 0) {
-      ASSERT(result == static_cast<int>(size));
-      result = result - static_cast<int>(addr.Size_());
+      ASSERT(result == static_cast<int>(encoded_size));
+      result = result - static_cast<int>(addrlength);
     }
     return result;
   }
 
-  virtual int Recv(void *pv, size_t cb) {
+  virtual int Recv(void* data, size_t size) {
     SocketAddress addr;
-    return RecvFrom(pv, cb, &addr);
+    return RecvFrom(data, size, &addr);
   }
 
-  virtual int RecvFrom(void *pv, size_t cb, SocketAddress *paddr) {
+  virtual int RecvFrom(void* data, size_t size, SocketAddress *out_addr) {
     if (server_addr_.IsAny() || type_ == SOCK_STREAM) {
-      return socket_->RecvFrom(pv, cb, paddr);
+      return socket_->RecvFrom(data, size, out_addr);
     }
-
     // Make sure we have enough room to read the requested amount plus the
-    // header address.
+    // largest possible header address.
     SocketAddress remote_addr;
-    Grow(cb + remote_addr.Size_());
+    Grow(size + kNATEncodedIPv6AddressSize);
 
     // Read the packet from the socket.
     int result = socket_->RecvFrom(buf_, size_, &remote_addr);
@@ -145,14 +194,15 @@
 
       // Decode the wire packet into the actual results.
       SocketAddress real_remote_addr;
-      size_t real_size = cb;
-      Decode(buf_, result, pv, &real_size, &real_remote_addr);
+      size_t addrlength =
+          UnpackAddressFromNAT(buf_, result, &real_remote_addr);
+      std::memcpy(data, buf_ + addrlength, result - addrlength);
 
       // Make sure this packet should be delivered before returning it.
       if (!connected_ || (real_remote_addr == remote_addr_)) {
-        if (paddr)
-          *paddr = real_remote_addr;
-        result = real_size;
+        if (out_addr)
+          *out_addr = real_remote_addr;
+        result = result - addrlength;
       } else {
         LOG(LS_ERROR) << "Dropping packet from unknown remote address: "
                       << real_remote_addr.ToString();
@@ -243,8 +293,8 @@
   // Sends the destination address to the server to tell it to connect.
   void SendConnectRequest() {
     char buf[256];
-    remote_addr_.Write_(buf, ARRAY_SIZE(buf));
-    socket_->Send(buf, remote_addr_.Size_());
+    size_t length = PackAddressForNAT(buf, ARRAY_SIZE(buf), remote_addr_);
+    socket_->Send(buf, length);
   }
 
   // Handles the byte sent back from the server and fires the appropriate event.
@@ -259,26 +309,6 @@
     }
   }
 
-  // Encodes the given data and intended remote address into a packet to send
-  // to the NAT server.
-  static void Encode(const char* data, size_t data_size, char* buf,
-                     size_t buf_size, const SocketAddress& remote_addr) {
-    ASSERT(buf_size == data_size + remote_addr.Size_());
-    remote_addr.Write_(buf, static_cast<int>(buf_size));
-    std::memcpy(buf + remote_addr.Size_(), data, data_size);
-  }
-
-  // Decodes the given packet from the NAT server into the actual remote
-  // address and data.
-  static void Decode(const char* data, size_t data_size, void* buf,
-                     size_t* buf_size, SocketAddress* remote_addr) {
-    ASSERT(data_size >= remote_addr->Size_());
-    ASSERT(data_size <= *buf_size + remote_addr->Size_());
-    remote_addr->Read_(data, static_cast<int>(data_size));
-    *buf_size = data_size - remote_addr->Size_();
-    std::memcpy(buf, data + remote_addr->Size_(), *buf_size);
-  }
-
   NATInternalSocketFactory* sf_;
   int type_;
   bool async_;
diff --git a/talk/base/natsocketfactory.h b/talk/base/natsocketfactory.h
index bbaaf3e..d0566d3 100644
--- a/talk/base/natsocketfactory.h
+++ b/talk/base/natsocketfactory.h
@@ -38,6 +38,9 @@
 
 namespace talk_base {
 
+const size_t kNATEncodedIPv4AddressSize = 8U;
+const size_t kNATEncodedIPv6AddressSize = 20U;
+
 // Used by the NAT socket implementation.
 class NATInternalSocketFactory {
  public:
@@ -112,7 +115,7 @@
 
     Translator* GetTranslator(const SocketAddress& ext_ip);
     Translator* AddTranslator(const SocketAddress& ext_ip,
-                              const SocketAddress& int_ip, NATType type);      
+                              const SocketAddress& int_ip, NATType type);
     void RemoveTranslator(const SocketAddress& ext_ip);
 
     bool AddClient(const SocketAddress& int_ip);
@@ -164,6 +167,11 @@
   DISALLOW_EVIL_CONSTRUCTORS(NATSocketServer);
 };
 
+// Free-standing NAT helper functions.
+size_t PackAddressForNAT(char* buf, size_t buf_size,
+                         const SocketAddress& remote_addr);
+size_t UnpackAddressFromNAT(const char* buf, size_t buf_size,
+                            SocketAddress* remote_addr);
 }  // namespace talk_base
 
 #endif  // TALK_BASE_NATSOCKETFACTORY_H_
diff --git a/talk/base/physicalsocketserver_unittest.cc b/talk/base/physicalsocketserver_unittest.cc
new file mode 100644
index 0000000..fa51e55
--- /dev/null
+++ b/talk/base/physicalsocketserver_unittest.cc
@@ -0,0 +1,224 @@
+/*
+ * libjingle
+ * Copyright 2004--2011, 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.
+ */
+
+#include <signal.h>
+#include <stdarg.h>
+
+#include "talk/base/gunit.h"
+#include "talk/base/logging.h"
+#include "talk/base/physicalsocketserver.h"
+#include "talk/base/scoped_ptr.h"
+#include "talk/base/socket_unittest.h"
+#include "talk/base/thread.h"
+
+namespace talk_base {
+
+class PhysicalSocketTest : public SocketTest {
+};
+
+TEST_F(PhysicalSocketTest, TestConnect) {
+  SocketTest::TestConnect();
+}
+
+TEST_F(PhysicalSocketTest, TestConnectWithDnsLookup) {
+  SocketTest::TestConnectWithDnsLookup();
+}
+
+TEST_F(PhysicalSocketTest, TestConnectFail) {
+  SocketTest::TestConnectFail();
+}
+
+TEST_F(PhysicalSocketTest, TestConnectWithDnsLookupFail) {
+  SocketTest::TestConnectWithDnsLookupFail();
+}
+
+#ifdef OSX
+// This test crashes the OS X kernel on 10.6 (at bsd/netinet/tcp_subr.c:2118).
+TEST_F(PhysicalSocketTest, DISABLED_TestConnectWithClosedSocket) {
+#else
+TEST_F(PhysicalSocketTest, TestConnectWithClosedSocket) {
+#endif
+  SocketTest::TestConnectWithClosedSocket();
+}
+
+TEST_F(PhysicalSocketTest, TestServerCloseDuringConnect) {
+  SocketTest::TestServerCloseDuringConnect();
+}
+
+TEST_F(PhysicalSocketTest, TestClientCloseDuringConnect) {
+  SocketTest::TestClientCloseDuringConnect();
+}
+
+TEST_F(PhysicalSocketTest, TestServerClose) {
+  SocketTest::TestServerClose();
+}
+
+TEST_F(PhysicalSocketTest, TestCloseInClosedCallback) {
+  SocketTest::TestCloseInClosedCallback();
+}
+
+TEST_F(PhysicalSocketTest, TestSocketServerWait) {
+  SocketTest::TestSocketServerWait();
+}
+
+TEST_F(PhysicalSocketTest, TestTcp) {
+  SocketTest::TestTcp();
+}
+
+TEST_F(PhysicalSocketTest, TestUdp) {
+  SocketTest::TestUdp();
+}
+
+TEST_F(PhysicalSocketTest, TestGetSetOptions) {
+  SocketTest::TestGetSetOptions();
+}
+
+#ifdef POSIX
+
+class PosixSignalDeliveryTest : public testing::Test {
+ public:
+  static void RecordSignal(int signum) {
+    signals_received_.push_back(signum);
+    signaled_thread_ = Thread::Current();
+  }
+
+ protected:
+  void SetUp() {
+    ss_.reset(new PhysicalSocketServer());
+  }
+
+  void TearDown() {
+    ss_.reset(NULL);
+    signals_received_.clear();
+    signaled_thread_ = NULL;
+  }
+
+  bool ExpectSignal(int signum) {
+    if (signals_received_.empty()) {
+      LOG(LS_ERROR) << "ExpectSignal(): No signal received";
+      return false;
+    }
+    if (signals_received_[0] != signum) {
+      LOG(LS_ERROR) << "ExpectSignal(): Received signal " <<
+          signals_received_[0] << ", expected " << signum;
+      return false;
+    }
+    signals_received_.erase(signals_received_.begin());
+    return true;
+  }
+
+  bool ExpectNone() {
+    bool ret = signals_received_.empty();
+    if (!ret) {
+      LOG(LS_ERROR) << "ExpectNone(): Received signal " << signals_received_[0]
+          << ", expected none";
+    }
+    return ret;
+  }
+
+  static std::vector<int> signals_received_;
+  static Thread *signaled_thread_;
+
+  scoped_ptr<PhysicalSocketServer> ss_;
+};
+
+std::vector<int> PosixSignalDeliveryTest::signals_received_;
+Thread *PosixSignalDeliveryTest::signaled_thread_ = NULL;
+
+// Test receiving a synchronous signal while not in Wait() and then entering
+// Wait() afterwards.
+TEST_F(PosixSignalDeliveryTest, RaiseThenWait) {
+  ss_->SetPosixSignalHandler(SIGTERM, &RecordSignal);
+  raise(SIGTERM);
+  EXPECT_TRUE(ss_->Wait(0, true));
+  EXPECT_TRUE(ExpectSignal(SIGTERM));
+  EXPECT_TRUE(ExpectNone());
+}
+
+// Test that we can handle getting tons of repeated signals and that we see all
+// the different ones.
+TEST_F(PosixSignalDeliveryTest, InsanelyManySignals) {
+  ss_->SetPosixSignalHandler(SIGTERM, &RecordSignal);
+  ss_->SetPosixSignalHandler(SIGINT, &RecordSignal);
+  for (int i = 0; i < 10000; ++i) {
+    raise(SIGTERM);
+  }
+  raise(SIGINT);
+  EXPECT_TRUE(ss_->Wait(0, true));
+  // Order will be lowest signal numbers first.
+  EXPECT_TRUE(ExpectSignal(SIGINT));
+  EXPECT_TRUE(ExpectSignal(SIGTERM));
+  EXPECT_TRUE(ExpectNone());
+}
+
+// Test that a signal during a Wait() call is detected.
+TEST_F(PosixSignalDeliveryTest, SignalDuringWait) {
+  ss_->SetPosixSignalHandler(SIGALRM, &RecordSignal);
+  alarm(1);
+  EXPECT_TRUE(ss_->Wait(1500, true));
+  EXPECT_TRUE(ExpectSignal(SIGALRM));
+  EXPECT_TRUE(ExpectNone());
+}
+
+class RaiseSigTermRunnable : public Runnable {
+  void Run(Thread *thread) {
+    thread->socketserver()->Wait(1000, false);
+
+    // Allow SIGTERM. This will be the only thread with it not masked so it will
+    // be delivered to us.
+    sigset_t mask;
+    sigemptyset(&mask);
+    pthread_sigmask(SIG_SETMASK, &mask, NULL);
+
+    // Raise it.
+    raise(SIGTERM);
+  }
+};
+
+// Test that it works no matter what thread the kernel chooses to give the
+// signal to (since it's not guaranteed to be the one that Wait() runs on).
+TEST_F(PosixSignalDeliveryTest, SignalOnDifferentThread) {
+  ss_->SetPosixSignalHandler(SIGTERM, &RecordSignal);
+  // Mask out SIGTERM so that it can't be delivered to this thread.
+  sigset_t mask;
+  sigemptyset(&mask);
+  sigaddset(&mask, SIGTERM);
+  EXPECT_EQ(0, pthread_sigmask(SIG_SETMASK, &mask, NULL));
+  // Start a new thread that raises it. It will have to be delivered to that
+  // thread. Our implementation should safely handle it and dispatch
+  // RecordSignal() on this thread.
+  scoped_ptr<Thread> thread(new Thread());
+  thread->Start(new RaiseSigTermRunnable());
+  EXPECT_TRUE(ss_->Wait(1500, true));
+  EXPECT_TRUE(ExpectSignal(SIGTERM));
+  EXPECT_EQ(Thread::Current(), signaled_thread_);
+  EXPECT_TRUE(ExpectNone());
+}
+
+#endif
+
+}  // namespace talk_base
diff --git a/talk/base/posix.cc b/talk/base/posix.cc
new file mode 100644
index 0000000..3502f6d
--- /dev/null
+++ b/talk/base/posix.cc
@@ -0,0 +1,148 @@
+/*
+ * libjingle
+ * Copyright 2004--2009, 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.
+ */
+
+#include "talk/base/posix.h"
+
+#include <sys/wait.h>
+#include <errno.h>
+#include <unistd.h>
+
+#ifdef LINUX
+#include "talk/base/linuxfdwalk.h"
+#endif
+#include "talk/base/logging.h"
+
+namespace talk_base {
+
+#ifdef LINUX
+static void closefds(void *close_errors, int fd) {
+  if (fd <= 2) {
+    // We leave stdin/out/err open to the browser's terminal, if any.
+    return;
+  }
+  if (close(fd) < 0) {
+    *static_cast<bool *>(close_errors) = true;
+  }
+}
+#endif
+
+enum {
+  EXIT_FLAG_CHDIR_ERRORS       = 1 << 0,
+#ifdef LINUX
+  EXIT_FLAG_FDWALK_ERRORS      = 1 << 1,
+  EXIT_FLAG_CLOSE_ERRORS       = 1 << 2,
+#endif
+  EXIT_FLAG_SECOND_FORK_FAILED = 1 << 3,
+};
+
+bool RunAsDaemon(const char *file, const char *const argv[]) {
+  // Fork intermediate child to daemonize.
+  pid_t pid = fork();
+  if (pid < 0) {
+    LOG_ERR(LS_ERROR) << "fork()";
+    return false;
+  } else if (!pid) {
+    // Child.
+
+    // We try to close all fds and change directory to /, but if that fails we
+    // keep going because it's not critical.
+    int exit_code = 0;
+    if (chdir("/") < 0) {
+      exit_code |= EXIT_FLAG_CHDIR_ERRORS;
+    }
+#ifdef LINUX
+    bool close_errors = false;
+    if (fdwalk(&closefds, &close_errors) < 0) {
+      exit_code |= EXIT_FLAG_FDWALK_ERRORS;
+    }
+    if (close_errors) {
+      exit_code |= EXIT_FLAG_CLOSE_ERRORS;
+    }
+#endif
+
+    // Fork again to become a daemon.
+    pid = fork();
+    // It is important that everything here use _exit() and not exit(), because
+    // exit() would call the destructors of all global variables in the whole
+    // process, which is both unnecessary and unsafe.
+    if (pid < 0) {
+      exit_code |= EXIT_FLAG_SECOND_FORK_FAILED;
+      _exit(exit_code);  // if second fork failed
+    } else if (!pid) {
+      // Child.
+      // Successfully daemonized. Run command.
+      // POSIX requires the args to be typed as non-const for historical
+      // reasons, but it mandates that the actual implementation be const, so
+      // the cast is safe.
+      execvp(file, const_cast<char *const *>(argv));
+      _exit(255);  // if execvp failed
+    }
+
+    // Parent.
+    // Successfully spawned process, but report any problems to the parent where
+    // we can log them.
+    _exit(exit_code);
+  }
+
+  // Parent. Reap intermediate child.
+  int status;
+  pid_t child = waitpid(pid, &status, 0);
+  if (child < 0) {
+    LOG_ERR(LS_ERROR) << "Error in waitpid()";
+    return false;
+  }
+  if (child != pid) {
+    // Should never happen (see man page).
+    LOG(LS_ERROR) << "waitpid() chose wrong child???";
+    return false;
+  }
+  if (!WIFEXITED(status)) {
+    LOG(LS_ERROR) << "Intermediate child killed uncleanly";  // Probably crashed
+    return false;
+  }
+
+  int exit_code = WEXITSTATUS(status);
+  if (exit_code & EXIT_FLAG_CHDIR_ERRORS) {
+    LOG(LS_WARNING) << "Child reported probles calling chdir()";
+  }
+#ifdef LINUX
+  if (exit_code & EXIT_FLAG_FDWALK_ERRORS) {
+    LOG(LS_WARNING) << "Child reported problems calling fdwalk()";
+  }
+  if (exit_code & EXIT_FLAG_CLOSE_ERRORS) {
+    LOG(LS_WARNING) << "Child reported problems calling close()";
+  }
+#endif
+  if (exit_code & EXIT_FLAG_SECOND_FORK_FAILED) {
+    LOG(LS_ERROR) << "Failed to daemonize";
+    // This means the command was not launched, so failure.
+    return false;
+  }
+  return true;
+}
+
+}  // namespace talk_base
diff --git a/talk/base/posix.h b/talk/base/posix.h
new file mode 100644
index 0000000..15c2c90
--- /dev/null
+++ b/talk/base/posix.h
@@ -0,0 +1,42 @@
+/*
+ * libjingle
+ * Copyright 2004--2010, 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.
+ */
+
+#ifndef TALK_BASE_POSIX_H_
+#define TALK_BASE_POSIX_H_
+
+namespace talk_base {
+
+// Runs the given executable name as a daemon, so that it executes concurrently
+// with this process. Upon completion, the daemon process will automatically be
+// reaped by init(8), so an error exit status or a failure to start the
+// executable are not reported. Returns true if the daemon process was forked
+// successfully, else false.
+bool RunAsDaemon(const char *file, const char *const argv[]);
+
+}  // namespace talk_base
+
+#endif  // TALK_BASE_POSIX_H_
diff --git a/talk/base/scoped_autorelease_pool.mm b/talk/base/scoped_autorelease_pool.mm
new file mode 100644
index 0000000..4009c7b
--- /dev/null
+++ b/talk/base/scoped_autorelease_pool.mm
@@ -0,0 +1,42 @@
+/*
+ * libjingle
+ * Copyright 2008, 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.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "talk/base/scoped_autorelease_pool.h"
+
+namespace talk_base {
+
+ScopedAutoreleasePool::ScopedAutoreleasePool() {
+  pool_ = [[NSAutoreleasePool alloc] init];
+}
+
+ScopedAutoreleasePool::~ScopedAutoreleasePool() {
+  [pool_ drain];
+}
+
+}  // namespace talk_base
diff --git a/talk/base/socketaddress.cc b/talk/base/socketaddress.cc
index 42143f5..c338944 100644
--- a/talk/base/socketaddress.cc
+++ b/talk/base/socketaddress.cc
@@ -299,45 +299,6 @@
   return h;
 }
 
-size_t SocketAddress::Size_() const {
-  return ip_.Size() + sizeof(port_) + 2;
-}
-
-bool SocketAddress::Write_(char* buf, int len) const {
-  if (len < static_cast<int>(Size_()))
-    return false;
-  int family = ip_.family();
-  if (family != AF_INET && family != AF_INET6) {
-    return false;
-  }
-  buf[0] = 0;
-  SetBE16(buf + 2, port_);
-  if (family == AF_INET) {
-    buf[1] = kStunFamilyIPv4;
-    SetBE32(buf + 4, ip_.v4AddressAsHostOrderInteger());
-  } else if (family == AF_INET6) {
-    buf[1] = kStunFamilyIPv6;
-    in6_addr addr = ip_.ipv6_address();
-    memcpy((buf + 4), &addr.s6_addr, sizeof(addr.s6_addr));
-  }
-  return true;
-}
-
-bool SocketAddress::Read_(const char* buf, int len) {
-  if (len < static_cast<int>(Size_()) ||
-      (buf[1] != kStunFamilyIPv4 && buf[1] != kStunFamilyIPv6))
-    return false;
-  port_ = GetBE16(buf + 2);
-  if (buf[1] == kStunFamilyIPv4) {
-    ip_ = IPAddress(GetBE32(buf + 4));
-  } else if (buf[1] == kStunFamilyIPv6) {
-    in6_addr addr;
-    memcpy(&addr.s6_addr, (buf + 4), sizeof(addr.s6_addr));
-    ip_ = IPAddress(addr);
-  }
-  return true;
-}
-
 void SocketAddress::ToSockAddr(sockaddr_in* saddr) const {
   memset(saddr, 0, sizeof(*saddr));
   if (ip_.family() != AF_INET) {
diff --git a/talk/base/socketaddress.h b/talk/base/socketaddress.h
index 3ebf556..7ddd21f 100644
--- a/talk/base/socketaddress.h
+++ b/talk/base/socketaddress.h
@@ -170,16 +170,6 @@
   // Hashes this address into a small number.
   size_t Hash() const;
 
-  // Returns the size of this address when written (for STUN).
-  // TODO: Move STUN functions( Size_/Write_/Read_) out of this class.
-  size_t Size_() const;
-
-  // Writes this address into the given buffer, according to RFC 5389.
-  bool Write_(char* buf, int len) const;
-
-  // Reads this address from the given buffer, according to RFC 5389.
-  bool Read_(const char* buf, int len);
-
   // Write this address to a sockaddr_in.
   // If IPv6, will zero out the sockaddr_in and sets family to AF_UNSPEC.
   void ToSockAddr(sockaddr_in* saddr) const;
diff --git a/talk/base/socketaddress_unittest.cc b/talk/base/socketaddress_unittest.cc
index fcfd89c..6e2ef7f 100644
--- a/talk/base/socketaddress_unittest.cc
+++ b/talk/base/socketaddress_unittest.cc
@@ -232,32 +232,6 @@
   EXPECT_FALSE(SocketAddressFromSockAddrStorage(addr_storage, NULL));
 }
 
-TEST(SocketAddressTest, TestIPv4ToFromBuffer) {
-  SocketAddress from("1.2.3.4", 5678), addr;
-  char buf[20];
-  EXPECT_TRUE(from.Write_(buf, sizeof(buf)));
-  EXPECT_TRUE(addr.Read_(buf, sizeof(buf)));
-  EXPECT_FALSE(addr.IsUnresolvedIP());
-  EXPECT_EQ(AF_INET, addr.ipaddr().family());
-  EXPECT_EQ(IPAddress(0x01020304U), addr.ipaddr());
-  EXPECT_EQ(5678, addr.port());
-  EXPECT_EQ("", addr.hostname());
-  EXPECT_EQ("1.2.3.4:5678", addr.ToString());
-}
-
-TEST(SocketAddressTest, TestIPv6ToFromBuffer) {
-  SocketAddress from6(kTestV6AddrString, 5678), addr;
-  char buf[20];
-  EXPECT_TRUE(from6.Write_(buf, sizeof(buf)));
-  EXPECT_TRUE(addr.Read_(buf, sizeof(buf)));
-  EXPECT_FALSE(addr.IsUnresolvedIP());
-  EXPECT_EQ(AF_INET6, addr.ipaddr().family());
-  EXPECT_EQ(IPAddress(kTestV6Addr), addr.ipaddr());
-  EXPECT_EQ(5678, addr.port());
-  EXPECT_EQ("", addr.hostname());
-  EXPECT_EQ(kTestV6AddrFullString, addr.ToString());
-}
-
 TEST(SocketAddressTest, TestGoodResolve) {
   SocketAddress addr("localhost", 5678);
   int error;
diff --git a/talk/base/testbase64.h b/talk/base/testbase64.h
new file mode 100644
index 0000000..39dd00c
--- /dev/null
+++ b/talk/base/testbase64.h
@@ -0,0 +1,5 @@
+/* This file was generated by googleclient/talk/binary2header.sh */
+
+static unsigned char testbase64[] = {
+0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xff, 0xe1, 0x0d, 0x07, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x01, 0x0e, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x9e, 0x01, 0x0f, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0xc3, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xd4, 0x01, 0x28, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x01, 0x31, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xdc, 0x01, 0x32, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xf0, 0x01, 0x3c, 0x00, 0x02, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x01, 0x04, 0x02, 0x13, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x02, 0xc4, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x53, 0x4f, 0x4e, 0x59, 0x00, 0x44, 0x53, 0x43, 0x2d, 0x50, 0x32, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x20, 0x37, 0x2e, 0x30, 0x00, 0x32, 0x30, 0x30, 0x37, 0x3a, 0x30, 0x31, 0x3a, 0x33, 0x30, 0x20, 0x32, 0x33, 0x3a, 0x31, 0x30, 0x3a, 0x30, 0x34, 0x00, 0x4d, 0x61, 0x63, 0x20, 0x4f, 0x53, 0x20, 0x58, 0x20, 0x31, 0x30, 0x2e, 0x34, 0x2e, 0x38, 0x00, 0x00, 0x1c, 0x82, 0x9a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x6a, 0x82, 0x9d, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x72, 0x88, 0x22, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x88, 0x27, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x64, 0x00, 0x00, 0x90, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x32, 0x32, 0x30, 0x90, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x02, 0x7a, 0x90, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x02, 0x8e, 0x91, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x03, 0x00, 0x91, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xa2, 0x92, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xaa, 0x92, 0x05, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xb2, 0x92, 0x07, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x00, 0x00, 0x92, 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x92, 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0f, 0x00, 0x00, 0x92, 0x0a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xba, 0xa0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x31, 0x30, 0x30, 0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0xa0, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0xa3, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x0a, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x01, 0x90, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x0a, 0x32, 0x30, 0x30, 0x37, 0x3a, 0x30, 0x31, 0x3a, 0x32, 0x30, 0x20, 0x32, 0x33, 0x3a, 0x30, 0x35, 0x3a, 0x35, 0x32, 0x00, 0x32, 0x30, 0x30, 0x37, 0x3a, 0x30, 0x31, 0x3a, 0x32, 0x30, 0x20, 0x32, 0x33, 0x3a, 0x30, 0x35, 0x3a, 0x35, 0x32, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x01, 0x1a, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x12, 0x01, 0x1b, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x1a, 0x01, 0x28, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x22, 0x02, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x09, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xff, 0xed, 0x00, 0x0c, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x5f, 0x43, 0x4d, 0x00, 0x02, 0xff, 0xee, 0x00, 0x0e, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x00, 0x64, 0x80, 0x00, 0x00, 0x00, 0x01, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x0c, 0x08, 0x08, 0x08, 0x09, 0x08, 0x0c, 0x09, 0x09, 0x0c, 0x11, 0x0b, 0x0a, 0x0b, 0x11, 0x15, 0x0f, 0x0c, 0x0c, 0x0f, 0x15, 0x18, 0x13, 0x13, 0x15, 0x13, 0x13, 0x18, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x01, 0x0d, 0x0b, 0x0b, 0x0d, 0x0e, 0x0d, 0x10, 0x0e, 0x0e, 0x10, 0x14, 0x0e, 0x0e, 0x0e, 0x14, 0x14, 0x0e, 0x0e, 0x0e, 0x0e, 0x14, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0x64, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xdd, 0x00, 0x04, 0x00, 0x07, 0xff, 0xc4, 0x01, 0x3f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x02, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x01, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x01, 0x04, 0x01, 0x03, 0x02, 0x04, 0x02, 0x05, 0x07, 0x06, 0x08, 0x05, 0x03, 0x0c, 0x33, 0x01, 0x00, 0x02, 0x11, 0x03, 0x04, 0x21, 0x12, 0x31, 0x05, 0x41, 0x51, 0x61, 0x13, 0x22, 0x71, 0x81, 0x32, 0x06, 0x14, 0x91, 0xa1, 0xb1, 0x42, 0x23, 0x24, 0x15, 0x52, 0xc1, 0x62, 0x33, 0x34, 0x72, 0x82, 0xd1, 0x43, 0x07, 0x25, 0x92, 0x53, 0xf0, 0xe1, 0xf1, 0x63, 0x73, 0x35, 0x16, 0xa2, 0xb2, 0x83, 0x26, 0x44, 0x93, 0x54, 0x64, 0x45, 0xc2, 0xa3, 0x74, 0x36, 0x17, 0xd2, 0x55, 0xe2, 0x65, 0xf2, 0xb3, 0x84, 0xc3, 0xd3, 0x75, 0xe3, 0xf3, 0x46, 0x27, 0x94, 0xa4, 0x85, 0xb4, 0x95, 0xc4, 0xd4, 0xe4, 0xf4, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xd7, 0xe7, 0xf7, 0x11, 0x00, 0x02, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x05, 0x06, 0x07, 0x07, 0x06, 0x05, 0x35, 0x01, 0x00, 0x02, 0x11, 0x03, 0x21, 0x31, 0x12, 0x04, 0x41, 0x51, 0x61, 0x71, 0x22, 0x13, 0x05, 0x32, 0x81, 0x91, 0x14, 0xa1, 0xb1, 0x42, 0x23, 0xc1, 0x52, 0xd1, 0xf0, 0x33, 0x24, 0x62, 0xe1, 0x72, 0x82, 0x92, 0x43, 0x53, 0x15, 0x63, 0x73, 0x34, 0xf1, 0x25, 0x06, 0x16, 0xa2, 0xb2, 0x83, 0x07, 0x26, 0x35, 0xc2, 0xd2, 0x44, 0x93, 0x54, 0xa3, 0x17, 0x64, 0x45, 0x55, 0x36, 0x74, 0x65, 0xe2, 0xf2, 0xb3, 0x84, 0xc3, 0xd3, 0x75, 0xe3, 0xf3, 0x46, 0x94, 0xa4, 0x85, 0xb4, 0x95, 0xc4, 0xd4, 0xe4, 0xf4, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x27, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf2, 0xed, 0xb2, 0x8d, 0x4d, 0x45, 0xcd, 0x2f, 0x3f, 0x44, 0x68, 0x93, 0xc3, 0x58, 0xc8, 0xf1, 0x1f, 0x8a, 0x33, 0x86, 0xda, 0x58, 0xc1, 0xa0, 0x02, 0x4f, 0xc4, 0xa1, 0x69, 0xa5, 0x9b, 0x5b, 0x4b, 0x84, 0x73, 0xdf, 0xc9, 0x15, 0xf8, 0xe3, 0xd1, 0x0e, 0x07, 0x93, 0xf3, 0xd1, 0x0f, 0x1c, 0x17, 0xef, 0x2e, 0x3b, 0x5b, 0xdc, 0xff, 0x00, 0xdf, 0x42, 0xbf, 0x8f, 0x8e, 0xdc, 0x82, 0xca, 0xd8, 0x37, 0x11, 0xa9, 0x3d, 0x82, 0x69, 0x2b, 0xc4, 0x6d, 0xc9, 0x75, 0x25, 0xbc, 0xf7, 0xec, 0xa1, 0xb5, 0x74, 0x19, 0x5d, 0x2e, 0x8a, 0x9a, 0x4b, 0x89, 0x7d, 0xc4, 0x68, 0xc6, 0xf6, 0xfe, 0xb2, 0xa0, 0x30, 0x1d, 0x60, 0x86, 0x88, 0x8d, 0x49, 0x3e, 0x01, 0x11, 0x20, 0xa3, 0x8c, 0xb9, 0xb1, 0xaa, 0x62, 0xad, 0xbf, 0x18, 0x97, 0x43, 0x47, 0x1d, 0xd2, 0xaf, 0x04, 0xd9, 0xb8, 0xc8, 0x0d, 0x68, 0xe4, 0xf7, 0x3e, 0x48, 0xf1, 0x05, 0xbc, 0x25, 0xaa, 0x07, 0x71, 0xd9, 0x14, 0x78, 0xf6, 0x49, 0xb5, 0x90, 0xfd, 0xa7, 0xc6, 0x14, 0xfd, 0x1b, 0x1c, 0xff, 0x00, 0x4d, 0x8d, 0x2e, 0x73, 0x8c, 0x35, 0xa3, 0x52, 0x4f, 0x92, 0x48, 0xa6, 0x1a, 0x24, 0xb6, 0x2a, 0xfa, 0xa5, 0x9e, 0x60, 0x64, 0x39, 0x94, 0x13, 0xcb, 0x27, 0x73, 0x80, 0xf3, 0x0c, 0xf6, 0xff, 0x00, 0xd2, 0x5a, 0x78, 0xbf, 0x53, 0x71, 0xf6, 0x01, 0x75, 0xb6, 0x97, 0x6a, 0x25, 0xa1, 0xad, 0x1f, 0xf4, 0xb7, 0x23, 0x48, 0xb7, 0x94, 0x84, 0x97, 0x5b, 0xff, 0x00, 0x32, 0xa9, 0xdd, 0xfc, 0xed, 0x9b, 0x7e, 0x0d, 0x9e, 0x52, 0x4a, 0x95, 0x61, 0xff, 0xd0, 0xf3, 0x3b, 0xa7, 0x70, 0xee, 0x01, 0x8f, 0xb9, 0x59, 0xfa, 0x7e, 0xdf, 0xe4, 0xc8, 0xf9, 0x2a, 0xc2, 0x5c, 0x63, 0xc3, 0x54, 0x67, 0x87, 0x6e, 0x10, 0x35, 0x68, 0xd4, 0x79, 0x1e, 0x53, 0x4a, 0xe0, 0xdc, 0xe9, 0xb8, 0x1f, 0x6a, 0xda, 0x6c, 0x25, 0x94, 0x37, 0xb0, 0xd0, 0xb8, 0xad, 0x67, 0xe4, 0x55, 0x8a, 0x5b, 0x8b, 0x82, 0xc0, 0x6f, 0x76, 0x80, 0x34, 0x49, 0x05, 0x2e, 0x9e, 0xc6, 0x1c, 0x66, 0x31, 0xba, 0x10, 0x23, 0xe0, 0xaf, 0xe1, 0x61, 0x53, 0x43, 0x8d, 0x81, 0xb3, 0x67, 0xef, 0x9e, 0x49, 0x2a, 0x12, 0x6c, 0xb6, 0x63, 0x1a, 0x0c, 0x31, 0xba, 0x55, 0xcd, 0xac, 0xfa, 0x8e, 0xdf, 0x91, 0x6e, 0x91, 0xd9, 0xb3, 0xc9, 0x73, 0x90, 0x7a, 0xab, 0x6a, 0xc2, 0xa4, 0x60, 0xe2, 0x8f, 0xd2, 0x38, 0x03, 0x7d, 0x9e, 0x0d, 0xff, 0x00, 0xcc, 0xd6, 0xd3, 0x6b, 0x71, 0x67, 0xd2, 0x3e, 0x64, 0x72, 0xab, 0xdb, 0x8d, 0x54, 0x39, 0xc5, 0x83, 0x6b, 0x3d, 0xee, 0x2e, 0xd4, 0x92, 0x3c, 0x4a, 0x56, 0xba, 0xb4, 0x79, 0x5c, 0xf7, 0xb2, 0x96, 0x6c, 0x8d, 0xaf, 0x80, 0x48, 0x3c, 0xf0, 0xb2, 0x1f, 0x63, 0x9c, 0xe9, 0x3f, 0x24, 0x5c, 0xdb, 0xdd, 0x76, 0x43, 0xde, 0xfd, 0x5c, 0xe3, 0x24, 0xfc, 0x50, 0x00, 0x93, 0x0a, 0x78, 0x8a, 0x0d, 0x49, 0xca, 0xcf, 0x93, 0x63, 0x1b, 0x7d, 0xd7, 0x57, 0x50, 0xd5, 0xef, 0x70, 0x6b, 0x4f, 0xc7, 0x45, 0xdb, 0x74, 0x9e, 0x8d, 0x5e, 0x33, 0x83, 0xd8, 0x37, 0xdd, 0xc3, 0xac, 0x3d, 0xbf, 0x92, 0xc5, 0x5b, 0xea, 0xbf, 0xd5, 0x62, 0xc0, 0xdc, 0xbc, 0xbd, 0x2d, 0x22, 0x5a, 0xcf, 0xdd, 0x69, 0xff, 0x00, 0xd1, 0x8e, 0x5d, 0xa5, 0x38, 0xb5, 0xb0, 0x00, 0xc6, 0xc4, 0x24, 0x4a, 0xd6, 0x8d, 0x18, 0x04, 0x49, 0x88, 0x9e, 0x55, 0xd6, 0x61, 0xb0, 0xc1, 0x70, 0x32, 0xdd, 0x3c, 0x95, 0xda, 0xf1, 0xfe, 0xf5, 0x62, 0xbc, 0x76, 0x8e, 0x75, 0x28, 0x02, 0xa2, 0xe7, 0x7d, 0x92, 0xb9, 0x84, 0x96, 0x96, 0xda, 0xf7, 0x70, 0x12, 0x4e, 0x5a, 0xff, 0x00, 0xff, 0xd1, 0xf3, 0x7a, 0x21, 0xaf, 0xde, 0xef, 0xa2, 0x22, 0x55, 0xfc, 0x5a, 0xbd, 0x42, 0xfb, 0x08, 0xfa, 0x67, 0x4f, 0x82, 0xcd, 0x6d, 0x85, 0xc0, 0x56, 0x3b, 0x90, 0xb7, 0xf0, 0x2a, 0x0e, 0x63, 0x58, 0x3b, 0xf2, 0xa3, 0x9e, 0x8c, 0xb8, 0x86, 0xbe, 0x49, 0xf1, 0x2c, 0x0c, 0x86, 0xb4, 0x4c, 0x69, 0xe4, 0xaf, 0x6e, 0xcc, 0x6b, 0x7d, 0x46, 0xb3, 0x70, 0xec, 0x38, 0x51, 0x7d, 0x02, 0x8a, 0xc7, 0xa6, 0xd9, 0x20, 0x68, 0x0f, 0x8f, 0x8a, 0xcf, 0xc9, 0xc2, 0xea, 0x59, 0x5b, 0x48, 0xb0, 0x91, 0xae, 0xe6, 0xc9, 0x03, 0xc9, 0x30, 0x51, 0x66, 0xd4, 0x0d, 0xad, 0xbd, 0x5f, 0x53, 0xcc, 0x6b, 0xb6, 0x90, 0x5a, 0x3b, 0x83, 0x0b, 0x43, 0x17, 0x31, 0xd6, 0xc3, 0x6e, 0x12, 0x3b, 0x79, 0xac, 0xc1, 0x89, 0x47, 0xd9, 0xe8, 0x63, 0x98, 0x45, 0xed, 0x6c, 0x5a, 0xf1, 0xa0, 0x27, 0xc5, 0x5b, 0xc3, 0x6f, 0xa6, 0xe0, 0x1c, 0x7d, 0xb3, 0xa2, 0x69, 0x34, 0x7b, 0xae, 0x1a, 0x8d, 0x45, 0x17, 0x9d, 0xeb, 0xfd, 0x21, 0xd8, 0xb9, 0xae, 0xb5, 0x80, 0xbb, 0x1e, 0xd2, 0x5c, 0xd7, 0x78, 0x13, 0xf9, 0xae, 0x4b, 0xea, 0xc7, 0x4a, 0x39, 0xbd, 0x55, 0xb3, 0xed, 0x66, 0x38, 0xf5, 0x09, 0x22, 0x41, 0x23, 0xe8, 0x37, 0xfb, 0x4b, 0xa1, 0xeb, 0xd6, 0xfe, 0x88, 0x31, 0xbf, 0x41, 0xc0, 0xee, 0xd2, 0x74, 0x02, 0x78, 0x53, 0xfa, 0x97, 0x43, 0x19, 0x85, 0x65, 0xff, 0x00, 0x9d, 0x71, 0x33, 0xe4, 0x1a, 0x7d, 0x8d, 0x53, 0x42, 0x56, 0x35, 0x6b, 0xe5, 0x80, 0x06, 0xc7, 0x57, 0xa7, 0xc4, 0xa9, 0xdb, 0xb6, 0x81, 0x1f, 0xeb, 0xd9, 0x69, 0x56, 0xc2, 0xd0, 0x00, 0xe5, 0x55, 0xc0, 0x12, 0xc2, 0xd7, 0x4e, 0xa2, 0x5a, 0x7c, 0x0a, 0xd0, 0x63, 0x9a, 0xd1, 0xaf, 0xd2, 0xe2, 0x3c, 0x12, 0x62, 0x66, 0xc6, 0x42, 0x23, 0x5a, 0x49, 0x8f, 0x10, 0xa2, 0xd2, 0x3e, 0x28, 0x9d, 0xc4, 0x88, 0x09, 0x29, 0x16, 0xc3, 0x3c, 0x24, 0x8d, 0xe6, 0x92, 0x72, 0x1f, 0xff, 0xd2, 0xf3, 0xbb, 0xb0, 0xfe, 0xcb, 0x99, 0xe9, 0xce, 0xf6, 0x88, 0x2d, 0x77, 0x91, 0x5b, 0x3d, 0x3d, 0xd0, 0xe6, 0x90, 0xa9, 0x65, 0x57, 0x38, 0x95, 0xdd, 0xcb, 0x9a, 0x7d, 0xce, 0xf2, 0x3f, 0x44, 0x23, 0x60, 0x58, 0x76, 0xe9, 0xca, 0x8c, 0xea, 0x1b, 0x31, 0x02, 0x32, 0x23, 0xea, 0xee, 0xb1, 0xcd, 0xb0, 0xc7, 0x87, 0x74, 0x7a, 0xeb, 0x70, 0x1a, 0x71, 0xe1, 0xfe, 0xe4, 0x1c, 0x1d, 0xae, 0xe5, 0x69, 0xd8, 0xfa, 0x99, 0x50, 0x0d, 0x1a, 0xf7, 0x2a, 0x3a, 0x0c, 0xf4, 0x1a, 0x8e, 0xc7, 0x27, 0x5d, 0xbf, 0x18, 0x41, 0xdc, 0xc2, 0xf0, 0x7f, 0x74, 0xf6, 0x3a, 0x22, 0x66, 0xdb, 0x68, 0xc6, 0x80, 0x48, 0x6b, 0x88, 0x06, 0x39, 0x0d, 0xee, 0xaa, 0x1f, 0xb3, 0xd5, 0x1b, 0x83, 0xd8, 0x3b, 0x38, 0x8f, 0x69, 0xfe, 0xdf, 0xd1, 0x4d, 0x29, 0xa1, 0x4c, 0x7a, 0xf4, 0xbf, 0xa7, 0x92, 0xcf, 0xa5, 0x20, 0x08, 0xf3, 0xf6, 0xff, 0x00, 0x15, 0xbb, 0xd1, 0x31, 0xd9, 0x5e, 0x3d, 0x75, 0x56, 0x36, 0x88, 0x00, 0x81, 0xe0, 0x16, 0x5e, 0x55, 0x74, 0x3f, 0x00, 0x9d, 0xe0, 0xcc, 0x69, 0xe7, 0x3a, 0x2d, 0xbe, 0x90, 0x00, 0xa9, 0xae, 0xef, 0x1f, 0x95, 0x4b, 0x0d, 0x9a, 0xdc, 0xc7, 0x45, 0xfe, 0xb1, 0x7d, 0x60, 0xa7, 0xa1, 0xe0, 0x1f, 0x4e, 0x1d, 0x99, 0x69, 0x02, 0x9a, 0xcf, 0x1f, 0xca, 0x7b, 0xbf, 0x90, 0xc5, 0xc2, 0xb3, 0xeb, 0x57, 0xd6, 0x03, 0x6b, 0xae, 0x39, 0xb6, 0x82, 0xe3, 0x31, 0xa1, 0x68, 0xf2, 0x6b, 0x5c, 0x12, 0xfa, 0xe1, 0x91, 0x66, 0x47, 0x5d, 0xb8, 0x3b, 0x4f, 0x44, 0x36, 0xb6, 0x8f, 0x28, 0xdd, 0xff, 0x00, 0x7e, 0x46, 0xab, 0x12, 0x2b, 0x65, 0x55, 0x32, 0xa7, 0x62, 0xb6, 0xbd, 0xf7, 0x64, 0x10, 0xdb, 0x03, 0x9f, 0x1b, 0x9e, 0xc7, 0xd9, 0xb8, 0x3b, 0x1f, 0x67, 0xf3, 0x6c, 0x52, 0x80, 0xd7, 0x7d, 0x0f, 0xea, 0x7f, 0x5d, 0x1d, 0x67, 0xa6, 0x0b, 0x1e, 0x47, 0xda, 0x69, 0x3b, 0x2e, 0x03, 0xc7, 0xf3, 0x5f, 0x1f, 0xf0, 0x8b, 0xa1, 0x02, 0x46, 0xba, 0x79, 0xaf, 0x32, 0xff, 0x00, 0x16, 0xad, 0xca, 0x1d, 0x57, 0x2a, 0xdc, 0x79, 0x18, 0x41, 0xb0, 0xf6, 0x9e, 0xe4, 0x9f, 0xd0, 0x8f, 0xeb, 0x31, 0xab, 0xd2, 0x83, 0xa4, 0xcb, 0x8c, 0xb8, 0xa0, 0x42, 0x12, 0x7b, 0x67, 0x9f, 0x2f, 0xf5, 0x09, 0x26, 0x96, 0xc4, 0xce, 0xa9, 0x20, 0xa7, 0xff, 0xd3, 0xf3, 0x2f, 0xb4, 0x5d, 0xe9, 0x0a, 0xb7, 0x9f, 0x4c, 0x19, 0xdb, 0x3a, 0x2d, 0x5e, 0x94, 0xfd, 0xc4, 0xb7, 0xc5, 0x62, 0xf9, 0x2b, 0xfd, 0x2e, 0xe3, 0x5d, 0xe0, 0x7c, 0x13, 0x48, 0xd1, 0x92, 0x12, 0xa9, 0x0b, 0x7a, 0xbc, 0x2d, 0xc2, 0x7f, 0x92, 0x60, 0xab, 0x4e, 0x79, 0x2e, 0x00, 0xf0, 0xaa, 0xe1, 0xda, 0x3d, 0x43, 0xfc, 0xad, 0x55, 0xbb, 0x80, 0x79, 0x81, 0xa0, 0xe6, 0x54, 0x32, 0x6d, 0x02, 0xbe, 0xf3, 0x61, 0x81, 0xa8, 0x44, 0x14, 0x03, 0x59, 0x0e, 0x1c, 0xf6, 0x1f, 0xdc, 0xb2, 0xec, 0xa3, 0x23, 0x77, 0xe8, 0x6e, 0x70, 0xf2, 0x25, 0x1f, 0x1f, 0x17, 0xa9, 0x6d, 0x71, 0x36, 0x97, 0x47, 0x00, 0xa4, 0x02, 0xe0, 0x2c, 0x7c, 0xc1, 0xab, 0xd5, 0x31, 0x85, 0x35, 0xd4, 0xe6, 0x13, 0x02, 0xd6, 0x4b, 0x67, 0x48, 0x2b, 0xa9, 0xe9, 0x2e, 0x02, 0xb6, 0x4f, 0x82, 0xe5, 0x7a, 0x95, 0x19, 0xc6, 0x87, 0x3d, 0xfb, 0xa2, 0xb8, 0x79, 0x1e, 0x4d, 0x3b, 0x96, 0xcf, 0x4f, 0xbd, 0xcd, 0xa2, 0xa2, 0x1f, 0xa0, 0x82, 0xd3, 0xfc, 0x97, 0x05, 0x24, 0x36, 0x6b, 0xf3, 0x31, 0xa2, 0x35, 0x79, 0xef, 0xad, 0xf8, 0xae, 0xaf, 0xaf, 0xd8, 0xf2, 0xd8, 0x6d, 0xed, 0x6b, 0xda, 0x7b, 0x18, 0x1b, 0x5d, 0xff, 0x00, 0x52, 0xb1, 0x6d, 0xf0, 0x81, 0x31, 0xca, 0xf4, 0x6e, 0xb1, 0x80, 0xce, 0xb1, 0x84, 0xc0, 0x21, 0xb7, 0xd6, 0x77, 0x31, 0xd1, 0x27, 0xc1, 0xcd, 0xfe, 0xd2, 0xe3, 0xec, 0xe8, 0x1d, 0x45, 0x96, 0xb0, 0x9a, 0xb7, 0x87, 0x3f, 0x68, 0x2d, 0xf7, 0x01, 0x1f, 0xbe, 0xd1, 0xf4, 0x7f, 0xb4, 0xa4, 0x0d, 0x77, 0xbb, 0xfa, 0x8f, 0x80, 0x3a, 0x7f, 0x43, 0xaa, 0xe2, 0xdf, 0xd2, 0x65, 0x7e, 0x95, 0xe4, 0x0f, 0x1f, 0xa1, 0xfe, 0x6b, 0x16, 0x9f, 0x52, 0xfa, 0xc1, 0xd3, 0xba, 0x6d, 0x26, 0xdc, 0xac, 0x86, 0xd4, 0xd9, 0x0d, 0x31, 0x2e, 0x74, 0x9e, 0xdb, 0x59, 0x2e, 0x55, 0xe8, 0xc9, 0xb2, 0x96, 0xd5, 0x4b, 0x9f, 0xb8, 0x6d, 0xda, 0x1c, 0x04, 0x09, 0x03, 0xfe, 0x8a, 0xc6, 0xfa, 0xd3, 0xf5, 0x6a, 0xbe, 0xbb, 0x5b, 0x2e, 0xc6, 0xb5, 0x94, 0xe6, 0xd5, 0x20, 0x97, 0x7d, 0x1b, 0x1b, 0xf9, 0xad, 0x7c, 0x7d, 0x17, 0xb7, 0xf3, 0x1e, 0x92, 0x1b, 0x7f, 0xf8, 0xe0, 0x7d, 0x59, 0xdd, 0xfd, 0x32, 0xd8, 0x8f, 0xa5, 0xe8, 0x3a, 0x12, 0x5c, 0x3f, 0xfc, 0xc4, 0xfa, 0xc3, 0xb3, 0x77, 0xa7, 0x56, 0xed, 0xdb, 0x76, 0x7a, 0x8d, 0xdd, 0x1f, 0xbf, 0xfd, 0x44, 0x92, 0x56, 0x8f, 0xff, 0xd4, 0xf2, 0xe8, 0x86, 0x17, 0x1e, 0xfa, 0x04, 0x56, 0x4b, 0x43, 0x6c, 0x6f, 0x2d, 0xe5, 0x46, 0x01, 0x64, 0x2b, 0x14, 0x32, 0x5b, 0xb4, 0xa0, 0x52, 0x1d, 0xde, 0x9b, 0x94, 0xdb, 0xab, 0x6b, 0x81, 0xf7, 0x05, 0xb0, 0xd7, 0x07, 0xb2, 0x27, 0x55, 0xc6, 0x57, 0x65, 0xd8, 0x76, 0x6e, 0x64, 0xed, 0xee, 0x16, 0xce, 0x27, 0x57, 0x63, 0xda, 0x0c, 0xc2, 0x8e, 0x51, 0x67, 0x84, 0xfa, 0x1d, 0xdd, 0x62, 0xc7, 0x07, 0xe9, 0xf7, 0xa3, 0xd6, 0x6c, 0x02, 0x41, 0x55, 0x31, 0xf3, 0x2b, 0xb3, 0xba, 0x2b, 0x2e, 0x68, 0x24, 0x1d, 0x47, 0x64, 0xca, 0xa6, 0x50, 0x41, 0x65, 0x90, 0x6c, 0xb1, 0xa5, 0xae, 0x33, 0x23, 0x51, 0xe4, 0xab, 0x7d, 0x5d, 0xcb, 0xb6, 0xcc, 0x37, 0xd0, 0x40, 0x73, 0x71, 0xde, 0x58, 0x09, 0xe7, 0x6f, 0x2c, 0x44, 0xc9, 0xc9, 0xae, 0xba, 0x9d, 0x63, 0x88, 0x01, 0xa0, 0x95, 0x9d, 0xf5, 0x3f, 0x2a, 0xe6, 0x67, 0xdb, 0x50, 0x83, 0x55, 0xad, 0x36, 0x3e, 0x78, 0x10, 0x74, 0x77, 0xfd, 0x2d, 0xaa, 0x4c, 0x7d, 0x58, 0x73, 0x91, 0xa0, 0x0f, 0x51, 0x45, 0xb7, 0x33, 0xdd, 0x58, 0x69, 0x1d, 0xd8, 0x0c, 0x9f, 0x96, 0x88, 0x19, 0x99, 0x19, 0xac, 0xcf, 0xa3, 0xd2, 0xad, 0xb5, 0xdb, 0x76, 0x8f, 0xad, 0xc4, 0xea, 0xcf, 0xdf, 0x7e, 0xdf, 0xdd, 0xfc, 0xd5, 0xa3, 0x5e, 0x43, 0x2b, 0x6b, 0xb2, 0xad, 0x3b, 0x6a, 0xa4, 0x13, 0xa7, 0x04, 0xac, 0x7a, 0x6f, 0xb3, 0x23, 0x26, 0xcc, 0xfb, 0xb4, 0x75, 0x8e, 0x01, 0x83, 0xf7, 0x58, 0x3e, 0x8b, 0x53, 0xa7, 0x2a, 0x1a, 0x31, 0x42, 0x36, 0x5d, 0x4c, 0x9a, 0xf2, 0xdc, 0xc6, 0xfe, 0x98, 0xb4, 0x34, 0xcb, 0x48, 0x0a, 0x8f, 0xdb, 0xb2, 0xeb, 0x76, 0xd6, 0x07, 0x5c, 0x59, 0xc9, 0x64, 0x8f, 0x93, 0xa7, 0x73, 0x16, 0x83, 0xaf, 0x0e, 0xa4, 0x33, 0xef, 0x50, 0xc5, 0x0c, 0xda, 0x59, 0x10, 0x06, 0x8a, 0x2e, 0x29, 0x0e, 0xac, 0xc2, 0x31, 0x3d, 0x36, 0x69, 0x7e, 0xd6, 0xcc, 0xf5, 0x3d, 0x6f, 0xb3, 0xeb, 0x1b, 0x76, 0xef, 0x3b, 0xa3, 0xfa, 0xc9, 0x2b, 0x5f, 0x66, 0x6f, 0xa9, 0x1e, 0x73, 0xf2, 0x49, 0x2e, 0x39, 0xf7, 0x4f, 0xb7, 0x8d, 0xff, 0xd5, 0xf3, 0x26, 0xfe, 0x0a, 0xc5, 0x1b, 0xa7, 0xcb, 0xb2, 0xcf, 0x49, 0x03, 0xb2, 0x46, 0xee, 0xd9, 0xd9, 0xb3, 0xf4, 0x9f, 0x25, 0x4a, 0xdf, 0x4b, 0x77, 0xe8, 0x27, 0xd4, 0xef, 0x1c, 0x2a, 0x29, 0x26, 0xc5, 0x7c, 0x9d, 0x6c, 0x7f, 0xb7, 0x6e, 0x1b, 0x26, 0x7f, 0x05, 0xa3, 0xfe, 0x53, 0x8d, 0x62, 0x57, 0x30, 0x92, 0x12, 0xfa, 0x2f, 0x86, 0xdf, 0xa4, 0xec, 0x67, 0xfe, 0xd0, 0xf4, 0xff, 0x00, 0x4d, 0xfc, 0xdf, 0x78, 0xe1, 0x68, 0x7d, 0x54, 0x99, 0xbf, 0x6f, 0xf3, 0xbe, 0xdf, 0x8e, 0xdd, 0x7f, 0xef, 0xeb, 0x97, 0x49, 0x3e, 0x3b, 0x7f, 0x06, 0x2c, 0x9f, 0x37, 0x5f, 0xf0, 0x9f, 0x4c, 0xeb, 0x7b, 0xbf, 0x67, 0x55, 0xe8, 0xff, 0x00, 0x31, 0xbc, 0x7a, 0x9e, 0x31, 0xdb, 0xfe, 0x92, 0xae, 0x37, 0x7a, 0x4d, 0xdb, 0xe2, 0x17, 0x9d, 0xa4, 0xa3, 0xc9, 0xba, 0xfc, 0x7b, 0x7d, 0x5f, 0x52, 0xa7, 0x7e, 0xd1, 0x28, 0xf8, 0xf3, 0xb0, 0xc7, 0x32, 0xbc, 0x99, 0x24, 0xc5, 0xe3, 0xab, 0xeb, 0x1f, 0xa4, 0xf5, 0xfc, 0xe1, 0x25, 0xe4, 0xe9, 0x24, 0x97, 0xff, 0xd9, 0xff, 0xed, 0x2e, 0x1c, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x20, 0x33, 0x2e, 0x30, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x1c, 0x02, 0x00, 0x00, 0x02, 0x00, 0x02, 0x1c, 0x02, 0x78, 0x00, 0x1f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xfb, 0x09, 0xa6, 0xbd, 0x07, 0x4c, 0x2a, 0x36, 0x9d, 0x8f, 0xe2, 0xcc, 0x57, 0xa9, 0xac, 0x85, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xea, 0x00, 0x00, 0x00, 0x00, 0x1d, 0xb0, 0x3c, 0x3f, 0x78, 0x6d, 0x6c, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3d, 0x22, 0x31, 0x2e, 0x30, 0x22, 0x20, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x3d, 0x22, 0x55, 0x54, 0x46, 0x2d, 0x38, 0x22, 0x3f, 0x3e, 0x0a, 0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x45, 0x20, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20, 0x22, 0x2d, 0x2f, 0x2f, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x2f, 0x44, 0x54, 0x44, 0x20, 0x50, 0x4c, 0x49, 0x53, 0x54, 0x20, 0x31, 0x2e, 0x30, 0x2f, 0x2f, 0x45, 0x4e, 0x22, 0x20, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x44, 0x54, 0x44, 0x73, 0x2f, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x2d, 0x31, 0x2e, 0x30, 0x2e, 0x64, 0x74, 0x64, 0x22, 0x3e, 0x0a, 0x3c, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3d, 0x22, 0x31, 0x2e, 0x30, 0x22, 0x3e, 0x0a, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x48, 0x6f, 0x72, 0x69, 0x7a, 0x6f, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x48, 0x6f, 0x72, 0x69, 0x7a, 0x6f, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x32, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x31, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x53, 0x63, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x53, 0x63, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x31, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x56, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x56, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x32, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x56, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x56, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x31, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x73, 0x75, 0x62, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x61, 0x70, 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x30, 0x2e, 0x30, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x30, 0x2e, 0x30, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x33, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x35, 0x37, 0x36, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x70, 0x65, 0x72, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2e, 0x50, 0x4d, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x70, 0x65, 0x72, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x2d, 0x31, 0x38, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x2d, 0x31, 0x38, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x37, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x35, 0x39, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x50, 0x61, 0x70, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x50, 0x61, 0x70, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x6e, 0x61, 0x2d, 0x6c, 0x65, 0x74, 0x74, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x33, 0x2d, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x54, 0x31, 0x37, 0x3a, 0x34, 0x39, 0x3a, 0x33, 0x36, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x31, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x55, 0x6e, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x55, 0x6e, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x30, 0x2e, 0x30, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x30, 0x2e, 0x30, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x33, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x35, 0x37, 0x36, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x55, 0x6e, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x70, 0x65, 0x72, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x50, 0x4d, 0x55, 0x6e, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x61, 0x70, 0x65, 0x72, 0x52, 0x65, 0x63, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x2d, 0x31, 0x38, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x2d, 0x31, 0x38, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x37, 0x37, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x35, 0x39, 0x34, 0x3c, 0x2f, 0x72, 0x65, 0x61, 0x6c, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x69, 0x6e, 0x67, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x2d, 0x33, 0x30, 0x54, 0x32, 0x32, 0x3a, 0x30, 0x38, 0x3a, 0x34, 0x31, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x30, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x70, 0x64, 0x2e, 0x50, 0x4d, 0x50, 0x61, 0x70, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x41, 0x72, 0x72, 0x61, 0x79, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x70, 0x64, 0x2e, 0x50, 0x4d, 0x50, 0x61, 0x70, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x55, 0x53, 0x20, 0x4c, 0x65, 0x74, 0x74, 0x65, 0x72, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x6d, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x44, 0x61, 0x74, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x32, 0x30, 0x30, 0x33, 0x2d, 0x30, 0x37, 0x2d, 0x30, 0x31, 0x54, 0x31, 0x37, 0x3a, 0x34, 0x39, 0x3a, 0x33, 0x36, 0x5a, 0x3c, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x46, 0x6c, 0x61, 0x67, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x31, 0x3c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x09, 0x3c, 0x2f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x30, 0x30, 0x2e, 0x32, 0x30, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x63, 0x6b, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2f, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x70, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x41, 0x50, 0x49, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x30, 0x30, 0x2e, 0x32, 0x30, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x63, 0x6b, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2f, 0x3e, 0x0a, 0x09, 0x3c, 0x6b, 0x65, 0x79, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x3c, 0x2f, 0x6b, 0x65, 0x79, 0x3e, 0x0a, 0x09, 0x3c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x3c, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3e, 0x0a, 0x3c, 0x2f, 0x64, 0x69, 0x63, 0x74, 0x3e, 0x0a, 0x3c, 0x2f, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x3e, 0x0a, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x03, 0x00, 0x00, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x02, 0xde, 0x02, 0x40, 0xff, 0xee, 0xff, 0xee, 0x03, 0x06, 0x02, 0x52, 0x03, 0x67, 0x05, 0x28, 0x03, 0xfc, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x02, 0xd8, 0x02, 0x28, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x03, 0x03, 0x00, 0x00, 0x00, 0x01, 0x7f, 0xff, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x08, 0x00, 0x19, 0x01, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xf3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x27, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x2f, 0x66, 0x66, 0x00, 0x01, 0x00, 0x6c, 0x66, 0x66, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x2f, 0x66, 0x66, 0x00, 0x01, 0x00, 0xa1, 0x99, 0x9a, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x00, 0x00, 0x01, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x35, 0x00, 0x00, 0x00, 0x01, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x38, 0x42, 0x49, 0x4d, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x03, 0x45, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x08, 0x00, 0x44, 0x00, 0x53, 0x00, 0x43, 0x00, 0x30, 0x00, 0x32, 0x00, 0x33, 0x00, 0x32, 0x00, 0x35, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6e, 0x75, 0x6c, 0x6c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x4f, 0x62, 0x6a, 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x63, 0x74, 0x31, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x54, 0x6f, 0x70, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x65, 0x66, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x74, 0x6f, 0x6d, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x52, 0x67, 0x68, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x06, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x73, 0x56, 0x6c, 0x4c, 0x73, 0x00, 0x00, 0x00, 0x01, 0x4f, 0x62, 0x6a, 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x07, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x49, 0x44, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x44, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x0c, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x6f, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x00, 0x00, 0x00, 0x00, 0x54, 0x79, 0x70, 0x65, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x0a, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6d, 0x67, 0x20, 0x00, 0x00, 0x00, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x4f, 0x62, 0x6a, 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x63, 0x74, 0x31, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x54, 0x6f, 0x70, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x65, 0x66, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x74, 0x6f, 0x6d, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x52, 0x67, 0x68, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, 0x6c, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6e, 0x75, 0x6c, 0x6c, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x73, 0x67, 0x65, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x61, 0x6c, 0x74, 0x54, 0x61, 0x67, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x63, 0x65, 0x6c, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x49, 0x73, 0x48, 0x54, 0x4d, 0x4c, 0x62, 0x6f, 0x6f, 0x6c, 0x01, 0x00, 0x00, 0x00, 0x08, 0x63, 0x65, 0x6c, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x68, 0x6f, 0x72, 0x7a, 0x41, 0x6c, 0x69, 0x67, 0x6e, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x0f, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x48, 0x6f, 0x72, 0x7a, 0x41, 0x6c, 0x69, 0x67, 0x6e, 0x00, 0x00, 0x00, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x00, 0x00, 0x00, 0x09, 0x76, 0x65, 0x72, 0x74, 0x41, 0x6c, 0x69, 0x67, 0x6e, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x0f, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x56, 0x65, 0x72, 0x74, 0x41, 0x6c, 0x69, 0x67, 0x6e, 0x00, 0x00, 0x00, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x00, 0x00, 0x00, 0x0b, 0x62, 0x67, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x65, 0x6e, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x11, 0x45, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x42, 0x47, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x00, 0x00, 0x00, 0x00, 0x4e, 0x6f, 0x6e, 0x65, 0x00, 0x00, 0x00, 0x09, 0x74, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x73, 0x65, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x6c, 0x65, 0x66, 0x74, 0x4f, 0x75, 0x74, 0x73, 0x65, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x62, 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x4f, 0x75, 0x74, 0x73, 0x65, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x72, 0x69, 0x67, 0x68, 0x74, 0x4f, 0x75, 0x74, 0x73, 0x65, 0x74, 0x6c, 0x6f, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x09, 0xf9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x01, 0x2c, 0x00, 0x00, 0x75, 0x30, 0x00, 0x00, 0x09, 0xdd, 0x00, 0x18, 0x00, 0x01, 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xff, 0xed, 0x00, 0x0c, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x5f, 0x43, 0x4d, 0x00, 0x02, 0xff, 0xee, 0x00, 0x0e, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x00, 0x64, 0x80, 0x00, 0x00, 0x00, 0x01, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x0c, 0x08, 0x08, 0x08, 0x09, 0x08, 0x0c, 0x09, 0x09, 0x0c, 0x11, 0x0b, 0x0a, 0x0b, 0x11, 0x15, 0x0f, 0x0c, 0x0c, 0x0f, 0x15, 0x18, 0x13, 0x13, 0x15, 0x13, 0x13, 0x18, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x01, 0x0d, 0x0b, 0x0b, 0x0d, 0x0e, 0x0d, 0x10, 0x0e, 0x0e, 0x10, 0x14, 0x0e, 0x0e, 0x0e, 0x14, 0x14, 0x0e, 0x0e, 0x0e, 0x0e, 0x14, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0x64, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xdd, 0x00, 0x04, 0x00, 0x07, 0xff, 0xc4, 0x01, 0x3f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x02, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x01, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x01, 0x04, 0x01, 0x03, 0x02, 0x04, 0x02, 0x05, 0x07, 0x06, 0x08, 0x05, 0x03, 0x0c, 0x33, 0x01, 0x00, 0x02, 0x11, 0x03, 0x04, 0x21, 0x12, 0x31, 0x05, 0x41, 0x51, 0x61, 0x13, 0x22, 0x71, 0x81, 0x32, 0x06, 0x14, 0x91, 0xa1, 0xb1, 0x42, 0x23, 0x24, 0x15, 0x52, 0xc1, 0x62, 0x33, 0x34, 0x72, 0x82, 0xd1, 0x43, 0x07, 0x25, 0x92, 0x53, 0xf0, 0xe1, 0xf1, 0x63, 0x73, 0x35, 0x16, 0xa2, 0xb2, 0x83, 0x26, 0x44, 0x93, 0x54, 0x64, 0x45, 0xc2, 0xa3, 0x74, 0x36, 0x17, 0xd2, 0x55, 0xe2, 0x65, 0xf2, 0xb3, 0x84, 0xc3, 0xd3, 0x75, 0xe3, 0xf3, 0x46, 0x27, 0x94, 0xa4, 0x85, 0xb4, 0x95, 0xc4, 0xd4, 0xe4, 0xf4, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xd7, 0xe7, 0xf7, 0x11, 0x00, 0x02, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x05, 0x06, 0x07, 0x07, 0x06, 0x05, 0x35, 0x01, 0x00, 0x02, 0x11, 0x03, 0x21, 0x31, 0x12, 0x04, 0x41, 0x51, 0x61, 0x71, 0x22, 0x13, 0x05, 0x32, 0x81, 0x91, 0x14, 0xa1, 0xb1, 0x42, 0x23, 0xc1, 0x52, 0xd1, 0xf0, 0x33, 0x24, 0x62, 0xe1, 0x72, 0x82, 0x92, 0x43, 0x53, 0x15, 0x63, 0x73, 0x34, 0xf1, 0x25, 0x06, 0x16, 0xa2, 0xb2, 0x83, 0x07, 0x26, 0x35, 0xc2, 0xd2, 0x44, 0x93, 0x54, 0xa3, 0x17, 0x64, 0x45, 0x55, 0x36, 0x74, 0x65, 0xe2, 0xf2, 0xb3, 0x84, 0xc3, 0xd3, 0x75, 0xe3, 0xf3, 0x46, 0x94, 0xa4, 0x85, 0xb4, 0x95, 0xc4, 0xd4, 0xe4, 0xf4, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x27, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf2, 0xed, 0xb2, 0x8d, 0x4d, 0x45, 0xcd, 0x2f, 0x3f, 0x44, 0x68, 0x93, 0xc3, 0x58, 0xc8, 0xf1, 0x1f, 0x8a, 0x33, 0x86, 0xda, 0x58, 0xc1, 0xa0, 0x02, 0x4f, 0xc4, 0xa1, 0x69, 0xa5, 0x9b, 0x5b, 0x4b, 0x84, 0x73, 0xdf, 0xc9, 0x15, 0xf8, 0xe3, 0xd1, 0x0e, 0x07, 0x93, 0xf3, 0xd1, 0x0f, 0x1c, 0x17, 0xef, 0x2e, 0x3b, 0x5b, 0xdc, 0xff, 0x00, 0xdf, 0x42, 0xbf, 0x8f, 0x8e, 0xdc, 0x82, 0xca, 0xd8, 0x37, 0x11, 0xa9, 0x3d, 0x82, 0x69, 0x2b, 0xc4, 0x6d, 0xc9, 0x75, 0x25, 0xbc, 0xf7, 0xec, 0xa1, 0xb5, 0x74, 0x19, 0x5d, 0x2e, 0x8a, 0x9a, 0x4b, 0x89, 0x7d, 0xc4, 0x68, 0xc6, 0xf6, 0xfe, 0xb2, 0xa0, 0x30, 0x1d, 0x60, 0x86, 0x88, 0x8d, 0x49, 0x3e, 0x01, 0x11, 0x20, 0xa3, 0x8c, 0xb9, 0xb1, 0xaa, 0x62, 0xad, 0xbf, 0x18, 0x97, 0x43, 0x47, 0x1d, 0xd2, 0xaf, 0x04, 0xd9, 0xb8, 0xc8, 0x0d, 0x68, 0xe4, 0xf7, 0x3e, 0x48, 0xf1, 0x05, 0xbc, 0x25, 0xaa, 0x07, 0x71, 0xd9, 0x14, 0x78, 0xf6, 0x49, 0xb5, 0x90, 0xfd, 0xa7, 0xc6, 0x14, 0xfd, 0x1b, 0x1c, 0xff, 0x00, 0x4d, 0x8d, 0x2e, 0x73, 0x8c, 0x35, 0xa3, 0x52, 0x4f, 0x92, 0x48, 0xa6, 0x1a, 0x24, 0xb6, 0x2a, 0xfa, 0xa5, 0x9e, 0x60, 0x64, 0x39, 0x94, 0x13, 0xcb, 0x27, 0x73, 0x80, 0xf3, 0x0c, 0xf6, 0xff, 0x00, 0xd2, 0x5a, 0x78, 0xbf, 0x53, 0x71, 0xf6, 0x01, 0x75, 0xb6, 0x97, 0x6a, 0x25, 0xa1, 0xad, 0x1f, 0xf4, 0xb7, 0x23, 0x48, 0xb7, 0x94, 0x84, 0x97, 0x5b, 0xff, 0x00, 0x32, 0xa9, 0xdd, 0xfc, 0xed, 0x9b, 0x7e, 0x0d, 0x9e, 0x52, 0x4a, 0x95, 0x61, 0xff, 0xd0, 0xf3, 0x3b, 0xa7, 0x70, 0xee, 0x01, 0x8f, 0xb9, 0x59, 0xfa, 0x7e, 0xdf, 0xe4, 0xc8, 0xf9, 0x2a, 0xc2, 0x5c, 0x63, 0xc3, 0x54, 0x67, 0x87, 0x6e, 0x10, 0x35, 0x68, 0xd4, 0x79, 0x1e, 0x53, 0x4a, 0xe0, 0xdc, 0xe9, 0xb8, 0x1f, 0x6a, 0xda, 0x6c, 0x25, 0x94, 0x37, 0xb0, 0xd0, 0xb8, 0xad, 0x67, 0xe4, 0x55, 0x8a, 0x5b, 0x8b, 0x82, 0xc0, 0x6f, 0x76, 0x80, 0x34, 0x49, 0x05, 0x2e, 0x9e, 0xc6, 0x1c, 0x66, 0x31, 0xba, 0x10, 0x23, 0xe0, 0xaf, 0xe1, 0x61, 0x53, 0x43, 0x8d, 0x81, 0xb3, 0x67, 0xef, 0x9e, 0x49, 0x2a, 0x12, 0x6c, 0xb6, 0x63, 0x1a, 0x0c, 0x31, 0xba, 0x55, 0xcd, 0xac, 0xfa, 0x8e, 0xdf, 0x91, 0x6e, 0x91, 0xd9, 0xb3, 0xc9, 0x73, 0x90, 0x7a, 0xab, 0x6a, 0xc2, 0xa4, 0x60, 0xe2, 0x8f, 0xd2, 0x38, 0x03, 0x7d, 0x9e, 0x0d, 0xff, 0x00, 0xcc, 0xd6, 0xd3, 0x6b, 0x71, 0x67, 0xd2, 0x3e, 0x64, 0x72, 0xab, 0xdb, 0x8d, 0x54, 0x39, 0xc5, 0x83, 0x6b, 0x3d, 0xee, 0x2e, 0xd4, 0x92, 0x3c, 0x4a, 0x56, 0xba, 0xb4, 0x79, 0x5c, 0xf7, 0xb2, 0x96, 0x6c, 0x8d, 0xaf, 0x80, 0x48, 0x3c, 0xf0, 0xb2, 0x1f, 0x63, 0x9c, 0xe9, 0x3f, 0x24, 0x5c, 0xdb, 0xdd, 0x76, 0x43, 0xde, 0xfd, 0x5c, 0xe3, 0x24, 0xfc, 0x50, 0x00, 0x93, 0x0a, 0x78, 0x8a, 0x0d, 0x49, 0xca, 0xcf, 0x93, 0x63, 0x1b, 0x7d, 0xd7, 0x57, 0x50, 0xd5, 0xef, 0x70, 0x6b, 0x4f, 0xc7, 0x45, 0xdb, 0x74, 0x9e, 0x8d, 0x5e, 0x33, 0x83, 0xd8, 0x37, 0xdd, 0xc3, 0xac, 0x3d, 0xbf, 0x92, 0xc5, 0x5b, 0xea, 0xbf, 0xd5, 0x62, 0xc0, 0xdc, 0xbc, 0xbd, 0x2d, 0x22, 0x5a, 0xcf, 0xdd, 0x69, 0xff, 0x00, 0xd1, 0x8e, 0x5d, 0xa5, 0x38, 0xb5, 0xb0, 0x00, 0xc6, 0xc4, 0x24, 0x4a, 0xd6, 0x8d, 0x18, 0x04, 0x49, 0x88, 0x9e, 0x55, 0xd6, 0x61, 0xb0, 0xc1, 0x70, 0x32, 0xdd, 0x3c, 0x95, 0xda, 0xf1, 0xfe, 0xf5, 0x62, 0xbc, 0x76, 0x8e, 0x75, 0x28, 0x02, 0xa2, 0xe7, 0x7d, 0x92, 0xb9, 0x84, 0x96, 0x96, 0xda, 0xf7, 0x70, 0x12, 0x4e, 0x5a, 0xff, 0x00, 0xff, 0xd1, 0xf3, 0x7a, 0x21, 0xaf, 0xde, 0xef, 0xa2, 0x22, 0x55, 0xfc, 0x5a, 0xbd, 0x42, 0xfb, 0x08, 0xfa, 0x67, 0x4f, 0x82, 0xcd, 0x6d, 0x85, 0xc0, 0x56, 0x3b, 0x90, 0xb7, 0xf0, 0x2a, 0x0e, 0x63, 0x58, 0x3b, 0xf2, 0xa3, 0x9e, 0x8c, 0xb8, 0x86, 0xbe, 0x49, 0xf1, 0x2c, 0x0c, 0x86, 0xb4, 0x4c, 0x69, 0xe4, 0xaf, 0x6e, 0xcc, 0x6b, 0x7d, 0x46, 0xb3, 0x70, 0xec, 0x38, 0x51, 0x7d, 0x02, 0x8a, 0xc7, 0xa6, 0xd9, 0x20, 0x68, 0x0f, 0x8f, 0x8a, 0xcf, 0xc9, 0xc2, 0xea, 0x59, 0x5b, 0x48, 0xb0, 0x91, 0xae, 0xe6, 0xc9, 0x03, 0xc9, 0x30, 0x51, 0x66, 0xd4, 0x0d, 0xad, 0xbd, 0x5f, 0x53, 0xcc, 0x6b, 0xb6, 0x90, 0x5a, 0x3b, 0x83, 0x0b, 0x43, 0x17, 0x31, 0xd6, 0xc3, 0x6e, 0x12, 0x3b, 0x79, 0xac, 0xc1, 0x89, 0x47, 0xd9, 0xe8, 0x63, 0x98, 0x45, 0xed, 0x6c, 0x5a, 0xf1, 0xa0, 0x27, 0xc5, 0x5b, 0xc3, 0x6f, 0xa6, 0xe0, 0x1c, 0x7d, 0xb3, 0xa2, 0x69, 0x34, 0x7b, 0xae, 0x1a, 0x8d, 0x45, 0x17, 0x9d, 0xeb, 0xfd, 0x21, 0xd8, 0xb9, 0xae, 0xb5, 0x80, 0xbb, 0x1e, 0xd2, 0x5c, 0xd7, 0x78, 0x13, 0xf9, 0xae, 0x4b, 0xea, 0xc7, 0x4a, 0x39, 0xbd, 0x55, 0xb3, 0xed, 0x66, 0x38, 0xf5, 0x09, 0x22, 0x41, 0x23, 0xe8, 0x37, 0xfb, 0x4b, 0xa1, 0xeb, 0xd6, 0xfe, 0x88, 0x31, 0xbf, 0x41, 0xc0, 0xee, 0xd2, 0x74, 0x02, 0x78, 0x53, 0xfa, 0x97, 0x43, 0x19, 0x85, 0x65, 0xff, 0x00, 0x9d, 0x71, 0x33, 0xe4, 0x1a, 0x7d, 0x8d, 0x53, 0x42, 0x56, 0x35, 0x6b, 0xe5, 0x80, 0x06, 0xc7, 0x57, 0xa7, 0xc4, 0xa9, 0xdb, 0xb6, 0x81, 0x1f, 0xeb, 0xd9, 0x69, 0x56, 0xc2, 0xd0, 0x00, 0xe5, 0x55, 0xc0, 0x12, 0xc2, 0xd7, 0x4e, 0xa2, 0x5a, 0x7c, 0x0a, 0xd0, 0x63, 0x9a, 0xd1, 0xaf, 0xd2, 0xe2, 0x3c, 0x12, 0x62, 0x66, 0xc6, 0x42, 0x23, 0x5a, 0x49, 0x8f, 0x10, 0xa2, 0xd2, 0x3e, 0x28, 0x9d, 0xc4, 0x88, 0x09, 0x29, 0x16, 0xc3, 0x3c, 0x24, 0x8d, 0xe6, 0x92, 0x72, 0x1f, 0xff, 0xd2, 0xf3, 0xbb, 0xb0, 0xfe, 0xcb, 0x99, 0xe9, 0xce, 0xf6, 0x88, 0x2d, 0x77, 0x91, 0x5b, 0x3d, 0x3d, 0xd0, 0xe6, 0x90, 0xa9, 0x65, 0x57, 0x38, 0x95, 0xdd, 0xcb, 0x9a, 0x7d, 0xce, 0xf2, 0x3f, 0x44, 0x23, 0x60, 0x58, 0x76, 0xe9, 0xca, 0x8c, 0xea, 0x1b, 0x31, 0x02, 0x32, 0x23, 0xea, 0xee, 0xb1, 0xcd, 0xb0, 0xc7, 0x87, 0x74, 0x7a, 0xeb, 0x70, 0x1a, 0x71, 0xe1, 0xfe, 0xe4, 0x1c, 0x1d, 0xae, 0xe5, 0x69, 0xd8, 0xfa, 0x99, 0x50, 0x0d, 0x1a, 0xf7, 0x2a, 0x3a, 0x0c, 0xf4, 0x1a, 0x8e, 0xc7, 0x27, 0x5d, 0xbf, 0x18, 0x41, 0xdc, 0xc2, 0xf0, 0x7f, 0x74, 0xf6, 0x3a, 0x22, 0x66, 0xdb, 0x68, 0xc6, 0x80, 0x48, 0x6b, 0x88, 0x06, 0x39, 0x0d, 0xee, 0xaa, 0x1f, 0xb3, 0xd5, 0x1b, 0x83, 0xd8, 0x3b, 0x38, 0x8f, 0x69, 0xfe, 0xdf, 0xd1, 0x4d, 0x29, 0xa1, 0x4c, 0x7a, 0xf4, 0xbf, 0xa7, 0x92, 0xcf, 0xa5, 0x20, 0x08, 0xf3, 0xf6, 0xff, 0x00, 0x15, 0xbb, 0xd1, 0x31, 0xd9, 0x5e, 0x3d, 0x75, 0x56, 0x36, 0x88, 0x00, 0x81, 0xe0, 0x16, 0x5e, 0x55, 0x74, 0x3f, 0x00, 0x9d, 0xe0, 0xcc, 0x69, 0xe7, 0x3a, 0x2d, 0xbe, 0x90, 0x00, 0xa9, 0xae, 0xef, 0x1f, 0x95, 0x4b, 0x0d, 0x9a, 0xdc, 0xc7, 0x45, 0xfe, 0xb1, 0x7d, 0x60, 0xa7, 0xa1, 0xe0, 0x1f, 0x4e, 0x1d, 0x99, 0x69, 0x02, 0x9a, 0xcf, 0x1f, 0xca, 0x7b, 0xbf, 0x90, 0xc5, 0xc2, 0xb3, 0xeb, 0x57, 0xd6, 0x03, 0x6b, 0xae, 0x39, 0xb6, 0x82, 0xe3, 0x31, 0xa1, 0x68, 0xf2, 0x6b, 0x5c, 0x12, 0xfa, 0xe1, 0x91, 0x66, 0x47, 0x5d, 0xb8, 0x3b, 0x4f, 0x44, 0x36, 0xb6, 0x8f, 0x28, 0xdd, 0xff, 0x00, 0x7e, 0x46, 0xab, 0x12, 0x2b, 0x65, 0x55, 0x32, 0xa7, 0x62, 0xb6, 0xbd, 0xf7, 0x64, 0x10, 0xdb, 0x03, 0x9f, 0x1b, 0x9e, 0xc7, 0xd9, 0xb8, 0x3b, 0x1f, 0x67, 0xf3, 0x6c, 0x52, 0x80, 0xd7, 0x7d, 0x0f, 0xea, 0x7f, 0x5d, 0x1d, 0x67, 0xa6, 0x0b, 0x1e, 0x47, 0xda, 0x69, 0x3b, 0x2e, 0x03, 0xc7, 0xf3, 0x5f, 0x1f, 0xf0, 0x8b, 0xa1, 0x02, 0x46, 0xba, 0x79, 0xaf, 0x32, 0xff, 0x00, 0x16, 0xad, 0xca, 0x1d, 0x57, 0x2a, 0xdc, 0x79, 0x18, 0x41, 0xb0, 0xf6, 0x9e, 0xe4, 0x9f, 0xd0, 0x8f, 0xeb, 0x31, 0xab, 0xd2, 0x83, 0xa4, 0xcb, 0x8c, 0xb8, 0xa0, 0x42, 0x12, 0x7b, 0x67, 0x9f, 0x2f, 0xf5, 0x09, 0x26, 0x96, 0xc4, 0xce, 0xa9, 0x20, 0xa7, 0xff, 0xd3, 0xf3, 0x2f, 0xb4, 0x5d, 0xe9, 0x0a, 0xb7, 0x9f, 0x4c, 0x19, 0xdb, 0x3a, 0x2d, 0x5e, 0x94, 0xfd, 0xc4, 0xb7, 0xc5, 0x62, 0xf9, 0x2b, 0xfd, 0x2e, 0xe3, 0x5d, 0xe0, 0x7c, 0x13, 0x48, 0xd1, 0x92, 0x12, 0xa9, 0x0b, 0x7a, 0xbc, 0x2d, 0xc2, 0x7f, 0x92, 0x60, 0xab, 0x4e, 0x79, 0x2e, 0x00, 0xf0, 0xaa, 0xe1, 0xda, 0x3d, 0x43, 0xfc, 0xad, 0x55, 0xbb, 0x80, 0x79, 0x81, 0xa0, 0xe6, 0x54, 0x32, 0x6d, 0x02, 0xbe, 0xf3, 0x61, 0x81, 0xa8, 0x44, 0x14, 0x03, 0x59, 0x0e, 0x1c, 0xf6, 0x1f, 0xdc, 0xb2, 0xec, 0xa3, 0x23, 0x77, 0xe8, 0x6e, 0x70, 0xf2, 0x25, 0x1f, 0x1f, 0x17, 0xa9, 0x6d, 0x71, 0x36, 0x97, 0x47, 0x00, 0xa4, 0x02, 0xe0, 0x2c, 0x7c, 0xc1, 0xab, 0xd5, 0x31, 0x85, 0x35, 0xd4, 0xe6, 0x13, 0x02, 0xd6, 0x4b, 0x67, 0x48, 0x2b, 0xa9, 0xe9, 0x2e, 0x02, 0xb6, 0x4f, 0x82, 0xe5, 0x7a, 0x95, 0x19, 0xc6, 0x87, 0x3d, 0xfb, 0xa2, 0xb8, 0x79, 0x1e, 0x4d, 0x3b, 0x96, 0xcf, 0x4f, 0xbd, 0xcd, 0xa2, 0xa2, 0x1f, 0xa0, 0x82, 0xd3, 0xfc, 0x97, 0x05, 0x24, 0x36, 0x6b, 0xf3, 0x31, 0xa2, 0x35, 0x79, 0xef, 0xad, 0xf8, 0xae, 0xaf, 0xaf, 0xd8, 0xf2, 0xd8, 0x6d, 0xed, 0x6b, 0xda, 0x7b, 0x18, 0x1b, 0x5d, 0xff, 0x00, 0x52, 0xb1, 0x6d, 0xf0, 0x81, 0x31, 0xca, 0xf4, 0x6e, 0xb1, 0x80, 0xce, 0xb1, 0x84, 0xc0, 0x21, 0xb7, 0xd6, 0x77, 0x31, 0xd1, 0x27, 0xc1, 0xcd, 0xfe, 0xd2, 0xe3, 0xec, 0xe8, 0x1d, 0x45, 0x96, 0xb0, 0x9a, 0xb7, 0x87, 0x3f, 0x68, 0x2d, 0xf7, 0x01, 0x1f, 0xbe, 0xd1, 0xf4, 0x7f, 0xb4, 0xa4, 0x0d, 0x77, 0xbb, 0xfa, 0x8f, 0x80, 0x3a, 0x7f, 0x43, 0xaa, 0xe2, 0xdf, 0xd2, 0x65, 0x7e, 0x95, 0xe4, 0x0f, 0x1f, 0xa1, 0xfe, 0x6b, 0x16, 0x9f, 0x52, 0xfa, 0xc1, 0xd3, 0xba, 0x6d, 0x26, 0xdc, 0xac, 0x86, 0xd4, 0xd9, 0x0d, 0x31, 0x2e, 0x74, 0x9e, 0xdb, 0x59, 0x2e, 0x55, 0xe8, 0xc9, 0xb2, 0x96, 0xd5, 0x4b, 0x9f, 0xb8, 0x6d, 0xda, 0x1c, 0x04, 0x09, 0x03, 0xfe, 0x8a, 0xc6, 0xfa, 0xd3, 0xf5, 0x6a, 0xbe, 0xbb, 0x5b, 0x2e, 0xc6, 0xb5, 0x94, 0xe6, 0xd5, 0x20, 0x97, 0x7d, 0x1b, 0x1b, 0xf9, 0xad, 0x7c, 0x7d, 0x17, 0xb7, 0xf3, 0x1e, 0x92, 0x1b, 0x7f, 0xf8, 0xe0, 0x7d, 0x59, 0xdd, 0xfd, 0x32, 0xd8, 0x8f, 0xa5, 0xe8, 0x3a, 0x12, 0x5c, 0x3f, 0xfc, 0xc4, 0xfa, 0xc3, 0xb3, 0x77, 0xa7, 0x56, 0xed, 0xdb, 0x76, 0x7a, 0x8d, 0xdd, 0x1f, 0xbf, 0xfd, 0x44, 0x92, 0x56, 0x8f, 0xff, 0xd4, 0xf2, 0xe8, 0x86, 0x17, 0x1e, 0xfa, 0x04, 0x56, 0x4b, 0x43, 0x6c, 0x6f, 0x2d, 0xe5, 0x46, 0x01, 0x64, 0x2b, 0x14, 0x32, 0x5b, 0xb4, 0xa0, 0x52, 0x1d, 0xde, 0x9b, 0x94, 0xdb, 0xab, 0x6b, 0x81, 0xf7, 0x05, 0xb0, 0xd7, 0x07, 0xb2, 0x27, 0x55, 0xc6, 0x57, 0x65, 0xd8, 0x76, 0x6e, 0x64, 0xed, 0xee, 0x16, 0xce, 0x27, 0x57, 0x63, 0xda, 0x0c, 0xc2, 0x8e, 0x51, 0x67, 0x84, 0xfa, 0x1d, 0xdd, 0x62, 0xc7, 0x07, 0xe9, 0xf7, 0xa3, 0xd6, 0x6c, 0x02, 0x41, 0x55, 0x31, 0xf3, 0x2b, 0xb3, 0xba, 0x2b, 0x2e, 0x68, 0x24, 0x1d, 0x47, 0x64, 0xca, 0xa6, 0x50, 0x41, 0x65, 0x90, 0x6c, 0xb1, 0xa5, 0xae, 0x33, 0x23, 0x51, 0xe4, 0xab, 0x7d, 0x5d, 0xcb, 0xb6, 0xcc, 0x37, 0xd0, 0x40, 0x73, 0x71, 0xde, 0x58, 0x09, 0xe7, 0x6f, 0x2c, 0x44, 0xc9, 0xc9, 0xae, 0xba, 0x9d, 0x63, 0x88, 0x01, 0xa0, 0x95, 0x9d, 0xf5, 0x3f, 0x2a, 0xe6, 0x67, 0xdb, 0x50, 0x83, 0x55, 0xad, 0x36, 0x3e, 0x78, 0x10, 0x74, 0x77, 0xfd, 0x2d, 0xaa, 0x4c, 0x7d, 0x58, 0x73, 0x91, 0xa0, 0x0f, 0x51, 0x45, 0xb7, 0x33, 0xdd, 0x58, 0x69, 0x1d, 0xd8, 0x0c, 0x9f, 0x96, 0x88, 0x19, 0x99, 0x19, 0xac, 0xcf, 0xa3, 0xd2, 0xad, 0xb5, 0xdb, 0x76, 0x8f, 0xad, 0xc4, 0xea, 0xcf, 0xdf, 0x7e, 0xdf, 0xdd, 0xfc, 0xd5, 0xa3, 0x5e, 0x43, 0x2b, 0x6b, 0xb2, 0xad, 0x3b, 0x6a, 0xa4, 0x13, 0xa7, 0x04, 0xac, 0x7a, 0x6f, 0xb3, 0x23, 0x26, 0xcc, 0xfb, 0xb4, 0x75, 0x8e, 0x01, 0x83, 0xf7, 0x58, 0x3e, 0x8b, 0x53, 0xa7, 0x2a, 0x1a, 0x31, 0x42, 0x36, 0x5d, 0x4c, 0x9a, 0xf2, 0xdc, 0xc6, 0xfe, 0x98, 0xb4, 0x34, 0xcb, 0x48, 0x0a, 0x8f, 0xdb, 0xb2, 0xeb, 0x76, 0xd6, 0x07, 0x5c, 0x59, 0xc9, 0x64, 0x8f, 0x93, 0xa7, 0x73, 0x16, 0x83, 0xaf, 0x0e, 0xa4, 0x33, 0xef, 0x50, 0xc5, 0x0c, 0xda, 0x59, 0x10, 0x06, 0x8a, 0x2e, 0x29, 0x0e, 0xac, 0xc2, 0x31, 0x3d, 0x36, 0x69, 0x7e, 0xd6, 0xcc, 0xf5, 0x3d, 0x6f, 0xb3, 0xeb, 0x1b, 0x76, 0xef, 0x3b, 0xa3, 0xfa, 0xc9, 0x2b, 0x5f, 0x66, 0x6f, 0xa9, 0x1e, 0x73, 0xf2, 0x49, 0x2e, 0x39, 0xf7, 0x4f, 0xb7, 0x8d, 0xff, 0xd5, 0xf3, 0x26, 0xfe, 0x0a, 0xc5, 0x1b, 0xa7, 0xcb, 0xb2, 0xcf, 0x49, 0x03, 0xb2, 0x46, 0xee, 0xd9, 0xd9, 0xb3, 0xf4, 0x9f, 0x25, 0x4a, 0xdf, 0x4b, 0x77, 0xe8, 0x27, 0xd4, 0xef, 0x1c, 0x2a, 0x29, 0x26, 0xc5, 0x7c, 0x9d, 0x6c, 0x7f, 0xb7, 0x6e, 0x1b, 0x26, 0x7f, 0x05, 0xa3, 0xfe, 0x53, 0x8d, 0x62, 0x57, 0x30, 0x92, 0x12, 0xfa, 0x2f, 0x86, 0xdf, 0xa4, 0xec, 0x67, 0xfe, 0xd0, 0xf4, 0xff, 0x00, 0x4d, 0xfc, 0xdf, 0x78, 0xe1, 0x68, 0x7d, 0x54, 0x99, 0xbf, 0x6f, 0xf3, 0xbe, 0xdf, 0x8e, 0xdd, 0x7f, 0xef, 0xeb, 0x97, 0x49, 0x3e, 0x3b, 0x7f, 0x06, 0x2c, 0x9f, 0x37, 0x5f, 0xf0, 0x9f, 0x4c, 0xeb, 0x7b, 0xbf, 0x67, 0x55, 0xe8, 0xff, 0x00, 0x31, 0xbc, 0x7a, 0x9e, 0x31, 0xdb, 0xfe, 0x92, 0xae, 0x37, 0x7a, 0x4d, 0xdb, 0xe2, 0x17, 0x9d, 0xa4, 0xa3, 0xc9, 0xba, 0xfc, 0x7b, 0x7d, 0x5f, 0x52, 0xa7, 0x7e, 0xd1, 0x28, 0xf8, 0xf3, 0xb0, 0xc7, 0x32, 0xbc, 0x99, 0x24, 0xc5, 0xe3, 0xab, 0xeb, 0x1f, 0xa4, 0xf5, 0xfc, 0xe1, 0x25, 0xe4, 0xe9, 0x24, 0x97, 0xff, 0xd9, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x41, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x62, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x00, 0x00, 0x13, 0x00, 0x41, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x62, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x20, 0x00, 0x37, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00, 0x38, 0x42, 0x49, 0x4d, 0x04, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0xff, 0xe1, 0x15, 0x67, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x00, 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x20, 0x62, 0x65, 0x67, 0x69, 0x6e, 0x3d, 0x27, 0xef, 0xbb, 0xbf, 0x27, 0x20, 0x69, 0x64, 0x3d, 0x27, 0x57, 0x35, 0x4d, 0x30, 0x4d, 0x70, 0x43, 0x65, 0x68, 0x69, 0x48, 0x7a, 0x72, 0x65, 0x53, 0x7a, 0x4e, 0x54, 0x63, 0x7a, 0x6b, 0x63, 0x39, 0x64, 0x27, 0x3f, 0x3e, 0x0a, 0x3c, 0x3f, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2d, 0x78, 0x61, 0x70, 0x2d, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x20, 0x65, 0x73, 0x63, 0x3d, 0x22, 0x43, 0x52, 0x22, 0x3f, 0x3e, 0x0a, 0x3c, 0x78, 0x3a, 0x78, 0x61, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x3d, 0x27, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x3a, 0x6e, 0x73, 0x3a, 0x6d, 0x65, 0x74, 0x61, 0x2f, 0x27, 0x20, 0x78, 0x3a, 0x78, 0x61, 0x70, 0x74, 0x6b, 0x3d, 0x27, 0x58, 0x4d, 0x50, 0x20, 0x74, 0x6f, 0x6f, 0x6c, 0x6b, 0x69, 0x74, 0x20, 0x32, 0x2e, 0x38, 0x2e, 0x32, 0x2d, 0x33, 0x33, 0x2c, 0x20, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x20, 0x31, 0x2e, 0x35, 0x27, 0x3e, 0x0a, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x72, 0x64, 0x66, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x31, 0x39, 0x39, 0x39, 0x2f, 0x30, 0x32, 0x2f, 0x32, 0x32, 0x2d, 0x72, 0x64, 0x66, 0x2d, 0x73, 0x79, 0x6e, 0x74, 0x61, 0x78, 0x2d, 0x6e, 0x73, 0x23, 0x27, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x69, 0x58, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x58, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x27, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x70, 0x64, 0x66, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x64, 0x66, 0x2f, 0x31, 0x2e, 0x33, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x21, 0x2d, 0x2d, 0x20, 0x70, 0x64, 0x66, 0x3a, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x20, 0x69, 0x73, 0x20, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x64, 0x20, 0x2d, 0x2d, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x21, 0x2d, 0x2d, 0x20, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x3a, 0x43, 0x61, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x73, 0x20, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x64, 0x20, 0x2d, 0x2d, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x61, 0x70, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x21, 0x2d, 0x2d, 0x20, 0x78, 0x61, 0x70, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x73, 0x20, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x64, 0x20, 0x2d, 0x2d, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x61, 0x70, 0x4d, 0x4d, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x6d, 0x6d, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x78, 0x61, 0x70, 0x4d, 0x4d, 0x3a, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x3e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x3a, 0x64, 0x6f, 0x63, 0x69, 0x64, 0x3a, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x36, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x3c, 0x2f, 0x78, 0x61, 0x70, 0x4d, 0x4d, 0x3a, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x27, 0x75, 0x75, 0x69, 0x64, 0x3a, 0x32, 0x32, 0x64, 0x30, 0x32, 0x62, 0x30, 0x61, 0x2d, 0x62, 0x32, 0x34, 0x39, 0x2d, 0x31, 0x31, 0x64, 0x62, 0x2d, 0x38, 0x61, 0x66, 0x38, 0x2d, 0x39, 0x31, 0x64, 0x35, 0x34, 0x30, 0x33, 0x66, 0x39, 0x32, 0x66, 0x39, 0x27, 0x0a, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x64, 0x63, 0x3d, 0x27, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x70, 0x75, 0x72, 0x6c, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x64, 0x63, 0x2f, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x31, 0x2e, 0x31, 0x2f, 0x27, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x64, 0x63, 0x3a, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x41, 0x6c, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x6c, 0x69, 0x20, 0x78, 0x6d, 0x6c, 0x3a, 0x6c, 0x61, 0x6e, 0x67, 0x3d, 0x27, 0x78, 0x2d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x27, 0x3e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x6c, 0x69, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x41, 0x6c, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x3c, 0x2f, 0x64, 0x63, 0x3a, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x0a, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46, 0x3e, 0x0a, 0x3c, 0x2f, 0x78, 0x3a, 0x78, 0x61, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x20, 0x65, 0x6e, 0x64, 0x3d, 0x27, 0x77, 0x27, 0x3f, 0x3e, 0xff, 0xee, 0x00, 0x0e, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x00, 0x64, 0x40, 0x00, 0x00, 0x00, 0x01, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x04, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x06, 0x04, 0x03, 0x04, 0x06, 0x07, 0x05, 0x04, 0x04, 0x05, 0x07, 0x08, 0x06, 0x06, 0x07, 0x06, 0x06, 0x08, 0x0a, 0x08, 0x09, 0x09, 0x09, 0x09, 0x08, 0x0a, 0x0a, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0a, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x01, 0x04, 0x05, 0x05, 0x08, 0x07, 0x08, 0x0f, 0x0a, 0x0a, 0x0f, 0x14, 0x0e, 0x0e, 0x0e, 0x14, 0x14, 0x0e, 0x0e, 0x0e, 0x0e, 0x14, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x11, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0x64, 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xdd, 0x00, 0x04, 0x00, 0x0d, 0xff, 0xc4, 0x01, 0xa2, 0x00, 0x00, 0x00, 0x07, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x05, 0x03, 0x02, 0x06, 0x01, 0x00, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x01, 0x00, 0x02, 0x02, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x02, 0x06, 0x07, 0x03, 0x04, 0x02, 0x06, 0x02, 0x73, 0x01, 0x02, 0x03, 0x11, 0x04, 0x00, 0x05, 0x21, 0x12, 0x31, 0x41, 0x51, 0x06, 0x13, 0x61, 0x22, 0x71, 0x81, 0x14, 0x32, 0x91, 0xa1, 0x07, 0x15, 0xb1, 0x42, 0x23, 0xc1, 0x52, 0xd1, 0xe1, 0x33, 0x16, 0x62, 0xf0, 0x24, 0x72, 0x82, 0xf1, 0x25, 0x43, 0x34, 0x53, 0x92, 0xa2, 0xb2, 0x63, 0x73, 0xc2, 0x35, 0x44, 0x27, 0x93, 0xa3, 0xb3, 0x36, 0x17, 0x54, 0x64, 0x74, 0xc3, 0xd2, 0xe2, 0x08, 0x26, 0x83, 0x09, 0x0a, 0x18, 0x19, 0x84, 0x94, 0x45, 0x46, 0xa4, 0xb4, 0x56, 0xd3, 0x55, 0x28, 0x1a, 0xf2, 0xe3, 0xf3, 0xc4, 0xd4, 0xe4, 0xf4, 0x65, 0x75, 0x85, 0x95, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x37, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xd7, 0xe7, 0xf7, 0x38, 0x48, 0x58, 0x68, 0x78, 0x88, 0x98, 0xa8, 0xb8, 0xc8, 0xd8, 0xe8, 0xf8, 0x29, 0x39, 0x49, 0x59, 0x69, 0x79, 0x89, 0x99, 0xa9, 0xb9, 0xc9, 0xd9, 0xe9, 0xf9, 0x2a, 0x3a, 0x4a, 0x5a, 0x6a, 0x7a, 0x8a, 0x9a, 0xaa, 0xba, 0xca, 0xda, 0xea, 0xfa, 0x11, 0x00, 0x02, 0x02, 0x01, 0x02, 0x03, 0x05, 0x05, 0x04, 0x05, 0x06, 0x04, 0x08, 0x03, 0x03, 0x6d, 0x01, 0x00, 0x02, 0x11, 0x03, 0x04, 0x21, 0x12, 0x31, 0x41, 0x05, 0x51, 0x13, 0x61, 0x22, 0x06, 0x71, 0x81, 0x91, 0x32, 0xa1, 0xb1, 0xf0, 0x14, 0xc1, 0xd1, 0xe1, 0x23, 0x42, 0x15, 0x52, 0x62, 0x72, 0xf1, 0x33, 0x24, 0x34, 0x43, 0x82, 0x16, 0x92, 0x53, 0x25, 0xa2, 0x63, 0xb2, 0xc2, 0x07, 0x73, 0xd2, 0x35, 0xe2, 0x44, 0x83, 0x17, 0x54, 0x93, 0x08, 0x09, 0x0a, 0x18, 0x19, 0x26, 0x36, 0x45, 0x1a, 0x27, 0x64, 0x74, 0x55, 0x37, 0xf2, 0xa3, 0xb3, 0xc3, 0x28, 0x29, 0xd3, 0xe3, 0xf3, 0x84, 0x94, 0xa4, 0xb4, 0xc4, 0xd4, 0xe4, 0xf4, 0x65, 0x75, 0x85, 0x95, 0xa5, 0xb5, 0xc5, 0xd5, 0xe5, 0xf5, 0x46, 0x56, 0x66, 0x76, 0x86, 0x96, 0xa6, 0xb6, 0xc6, 0xd6, 0xe6, 0xf6, 0x47, 0x57, 0x67, 0x77, 0x87, 0x97, 0xa7, 0xb7, 0xc7, 0xd7, 0xe7, 0xf7, 0x38, 0x48, 0x58, 0x68, 0x78, 0x88, 0x98, 0xa8, 0xb8, 0xc8, 0xd8, 0xe8, 0xf8, 0x39, 0x49, 0x59, 0x69, 0x79, 0x89, 0x99, 0xa9, 0xb9, 0xc9, 0xd9, 0xe9, 0xf9, 0x2a, 0x3a, 0x4a, 0x5a, 0x6a, 0x7a, 0x8a, 0x9a, 0xaa, 0xba, 0xca, 0xda, 0xea, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf0, 0x67, 0xa6, 0x5c, 0x0f, 0x01, 0xd4, 0x7e, 0x18, 0x12, 0x98, 0xe9, 0xd6, 0x2d, 0x34, 0x6d, 0x70, 0xdf, 0xdc, 0xa1, 0xe3, 0xec, 0x5b, 0xfb, 0x32, 0x24, 0xb2, 0x01, 0x1f, 0x15, 0xa4, 0x52, 0x4a, 0x82, 0x31, 0xf1, 0xfe, 0xd1, 0x3d, 0x14, 0x64, 0x49, 0x64, 0x22, 0x98, 0xcf, 0xa5, 0x46, 0x6c, 0x16, 0x55, 0x71, 0x56, 0x62, 0x28, 0x07, 0xc5, 0x45, 0x15, 0xa0, 0xc8, 0x89, 0x33, 0xe1, 0x63, 0xd2, 0xd8, 0x34, 0x44, 0x17, 0xa0, 0x2c, 0x4d, 0x16, 0xbb, 0xed, 0xdc, 0xf8, 0x64, 0xc1, 0x6b, 0x31, 0x42, 0x18, 0x8e, 0xc7, 0xb5, 0x2a, 0x7d, 0xb2, 0x56, 0xc5, 0x61, 0x8c, 0xf2, 0xa0, 0x1b, 0x1e, 0x83, 0x0d, 0xa1, 0x63, 0x50, 0x1f, 0x97, 0x7c, 0x2a, 0xa9, 0x1a, 0x9a, 0x86, 0x4f, 0xb4, 0xb4, 0x38, 0x0a, 0xa6, 0x0b, 0xb8, 0x0c, 0x05, 0x14, 0xf8, 0x76, 0x3e, 0x19, 0x14, 0xb6, 0x78, 0xf8, 0x8c, 0x2a, 0xd5, 0x01, 0xdc, 0x6f, 0x8a, 0x1a, 0xe3, 0x8d, 0xab, 0xff, 0xd0, 0xf0, 0xec, 0xe9, 0x15, 0xb5, 0xb9, 0x5a, 0x7c, 0x4c, 0xa2, 0x9e, 0x24, 0xf5, 0xca, 0xc6, 0xe5, 0x99, 0xd9, 0x34, 0x99, 0x04, 0x3a, 0x7d, 0xb5, 0xba, 0xd5, 0x51, 0x63, 0x0e, 0xc7, 0xc5, 0x9b, 0x73, 0xf8, 0xe4, 0x6f, 0x76, 0xca, 0xd9, 0xda, 0x54, 0x6d, 0x72, 0x2e, 0x1a, 0x57, 0x11, 0x44, 0x40, 0x0d, 0x27, 0x7a, 0x0f, 0xd9, 0x5f, 0x12, 0x69, 0x4c, 0x84, 0xcd, 0x36, 0xe3, 0x85, 0xb2, 0xcd, 0x2f, 0x4a, 0x8b, 0x58, 0x36, 0xf6, 0x76, 0xa8, 0x64, 0x64, 0x3c, 0xa4, 0x93, 0xaa, 0x25, 0x3c, 0x49, 0xda, 0xa4, 0xe5, 0x26, 0x54, 0xe4, 0x8c, 0x7c, 0x5c, 0x93, 0x4d, 0x67, 0xc9, 0x3a, 0x6e, 0x9f, 0x13, 0xb4, 0xce, 0xf7, 0x3a, 0x9b, 0xad, 0x52, 0xd6, 0x2a, 0xd1, 0x49, 0xee, 0xc7, 0xf8, 0x64, 0x46, 0x42, 0x4e, 0xcd, 0x92, 0xc2, 0x00, 0xdd, 0x8a, 0x47, 0xe5, 0x69, 0x6e, 0xd4, 0xa4, 0x08, 0x16, 0x83, 0x9c, 0x8c, 0xdd, 0x95, 0x6b, 0xb9, 0xf6, 0xef, 0x97, 0x78, 0x94, 0xe3, 0x78, 0x04, 0xa4, 0xf3, 0xe8, 0xee, 0x64, 0xe1, 0x12, 0x10, 0x05, 0x6a, 0xc7, 0xc0, 0x6f, 0x53, 0xf3, 0xc9, 0x89, 0xb4, 0x9c, 0x4e, 0xb4, 0xf2, 0xd3, 0xde, 0x7a, 0xd2, 0x19, 0x16, 0x38, 0x61, 0x5d, 0xd9, 0x88, 0x05, 0x9c, 0xf4, 0x0a, 0x0f, 0x5f, 0x73, 0x84, 0xe4, 0xa4, 0xc7, 0x0d, 0xa5, 0xf1, 0x59, 0xba, 0x5c, 0x08, 0x98, 0x6f, 0xc8, 0x20, 0xfa, 0x4e, 0x4e, 0xf6, 0x69, 0xe1, 0xa2, 0x89, 0xfd, 0x1f, 0x77, 0x2c, 0xe6, 0xce, 0xd6, 0x17, 0x9a, 0x69, 0xdb, 0xd3, 0x86, 0x18, 0xc1, 0x67, 0x77, 0x26, 0x80, 0x28, 0x1b, 0x93, 0x88, 0x41, 0x0f, 0x40, 0xb0, 0xfc, 0x87, 0xf3, 0x43, 0x98, 0xd7, 0x58, 0x96, 0xdb, 0x4d, 0x91, 0x88, 0xe5, 0x6c, 0x58, 0xdc, 0x5c, 0x2a, 0xf7, 0x2c, 0xb1, 0xfc, 0x20, 0x8f, 0x02, 0xd9, 0x65, 0x06, 0xbe, 0x26, 0x6f, 0xa2, 0x7f, 0xce, 0x3d, 0x69, 0x26, 0xdd, 0x13, 0x52, 0xbf, 0xbd, 0x92, 0x62, 0x59, 0x4c, 0x90, 0xac, 0x50, 0x45, 0x5e, 0xbb, 0x09, 0x03, 0x12, 0x29, 0x84, 0x00, 0xc4, 0xc9, 0x11, 0xff, 0x00, 0x42, 0xe7, 0xa7, 0x7a, 0xd4, 0xfd, 0x21, 0x79, 0xe9, 0x78, 0x71, 0x8b, 0x95, 0x39, 0x75, 0xaf, 0x4e, 0x98, 0x78, 0x42, 0x38, 0xdf, 0xff, 0xd1, 0xf0, 0xe6, 0xa0, 0x58, 0xc8, 0x84, 0x9a, 0xaa, 0x30, 0x55, 0xf9, 0x0a, 0x6f, 0x90, 0x0c, 0xca, 0x72, 0x48, 0xb8, 0x1e, 0x89, 0xa7, 0x23, 0x17, 0x24, 0xff, 0x00, 0x61, 0xb6, 0x54, 0x76, 0x6e, 0x1b, 0xa7, 0xbe, 0x50, 0xf2, 0xc1, 0xd7, 0x4c, 0x52, 0x5e, 0x33, 0x5b, 0xe9, 0x10, 0xf4, 0x54, 0x3c, 0x5e, 0x77, 0xee, 0x49, 0xec, 0x2b, 0xb6, 0x63, 0xe4, 0xc9, 0xc3, 0xef, 0x73, 0xf0, 0xe1, 0x32, 0x1b, 0xf2, 0x7a, 0x05, 0xce, 0xad, 0x65, 0xa1, 0x98, 0xb4, 0x0f, 0x2a, 0x5b, 0x23, 0xeb, 0x12, 0x00, 0x88, 0xb0, 0xa8, 0x66, 0x46, 0x3d, 0xea, 0x7b, 0xfb, 0x9e, 0x99, 0x89, 0xbc, 0x8d, 0x97, 0x3a, 0x34, 0x05, 0x32, 0x5d, 0x1f, 0xc9, 0x1a, 0x8c, 0x36, 0x8c, 0x6f, 0x66, 0xfa, 0xc6, 0xb7, 0x7d, 0xf0, 0x94, 0x04, 0xf0, 0x88, 0xc9, 0xd5, 0x9d, 0x8d, 0x4b, 0x11, 0xd4, 0x9f, 0xbb, 0x25, 0xc5, 0xdc, 0xa2, 0x03, 0x99, 0x4b, 0xbc, 0xf3, 0x0d, 0x97, 0x96, 0x74, 0xe5, 0xf2, 0xb6, 0x80, 0x95, 0xbd, 0x99, 0x15, 0xf5, 0x4b, 0xd2, 0x37, 0x58, 0x46, 0xd4, 0x27, 0xc5, 0xce, 0xc1, 0x7c, 0x30, 0x8e, 0x68, 0x94, 0x7b, 0x9e, 0x6d, 0xe6, 0x7b, 0x9b, 0x5d, 0x3a, 0xd8, 0xdb, 0x32, 0xfa, 0x77, 0x65, 0x15, 0xe4, 0x57, 0xa7, 0x21, 0x55, 0x04, 0x57, 0xef, 0xd8, 0x66, 0x56, 0x38, 0x19, 0x1b, 0xe8, 0xe0, 0x67, 0x98, 0xc7, 0x1a, 0x1c, 0xde, 0x71, 0x71, 0x79, 0x2c, 0xf2, 0xfa, 0x8c, 0x48, 0xec, 0xb5, 0x24, 0x9a, 0x0c, 0xce, 0x75, 0x29, 0xae, 0x8c, 0x67, 0xd4, 0xb5, 0x0b, 0x4b, 0x04, 0x05, 0xef, 0x2e, 0x66, 0x8e, 0x18, 0x08, 0x15, 0xdd, 0x8f, 0x11, 0xb0, 0xeb, 0x4c, 0x04, 0x5b, 0x21, 0x2a, 0x7d, 0x41, 0xe4, 0x4f, 0xcb, 0xcb, 0x5d, 0x12, 0x45, 0xb8, 0xb7, 0x53, 0x71, 0xaa, 0x9f, 0x86, 0x5b, 0xd6, 0x50, 0x4a, 0xed, 0xba, 0x46, 0x77, 0x00, 0x13, 0xd4, 0x8c, 0x85, 0xd3, 0x12, 0x6d, 0xeb, 0x1a, 0x67, 0x95, 0xd9, 0x39, 0x39, 0x50, 0xac, 0xff, 0x00, 0x6f, 0xc4, 0xff, 0x00, 0x1c, 0x81, 0x92, 0xb2, 0x6b, 0x6d, 0x02, 0xdd, 0xbd, 0x36, 0x92, 0x36, 0x2d, 0x1f, 0xc0, 0x2a, 0x0b, 0x28, 0x1b, 0x91, 0x41, 0xf4, 0x9c, 0xb6, 0x25, 0x81, 0x46, 0xfe, 0x81, 0xb5, 0xad, 0x3d, 0xba, 0x57, 0xb7, 0xf9, 0xf6, 0xc9, 0xb0, 0x7f, 0xff, 0xd2, 0xf0, 0xe2, 0x86, 0x95, 0xc4, 0x67, 0x7e, 0x3f, 0x11, 0xf7, 0xa8, 0x19, 0x06, 0x69, 0x8d, 0xca, 0xca, 0x24, 0x8f, 0xd3, 0x52, 0x24, 0x89, 0x47, 0x25, 0x1f, 0xcb, 0x20, 0xf8, 0xb2, 0xb2, 0x76, 0x6e, 0x88, 0x36, 0xf6, 0x6f, 0x2a, 0xc1, 0x6e, 0xfa, 0x45, 0xad, 0xbc, 0x3f, 0x0b, 0x46, 0x81, 0x4d, 0x46, 0xea, 0x7a, 0x9a, 0x83, 0x9a, 0xa9, 0xdd, 0xbb, 0xec, 0x7b, 0x06, 0x5b, 0xe5, 0xcf, 0x2e, 0x69, 0xfa, 0x5c, 0xcd, 0x7b, 0x14, 0x5e, 0xa5, 0xee, 0xf5, 0xb8, 0x7d, 0xdd, 0x99, 0xba, 0xef, 0x91, 0x16, 0x5b, 0x36, 0xb6, 0x65, 0x0d, 0xac, 0xb2, 0x5b, 0xed, 0x34, 0x81, 0x7a, 0xbb, 0x46, 0x40, 0x6a, 0x9e, 0xb4, 0x39, 0x31, 0x13, 0x49, 0xda, 0xd2, 0x9b, 0xed, 0x1e, 0xc4, 0x24, 0xb3, 0x35, 0xb2, 0x88, 0x60, 0x06, 0xe6, 0x56, 0x98, 0x96, 0x79, 0x1e, 0x31, 0x51, 0xc9, 0x8f, 0xcb, 0x00, 0xe6, 0xb3, 0xe4, 0xf9, 0x2b, 0xcc, 0x7a, 0x94, 0xda, 0x96, 0xa9, 0x71, 0x77, 0x70, 0x79, 0xcd, 0x33, 0x97, 0x76, 0x3f, 0xcc, 0xc6, 0xa6, 0x9f, 0x2e, 0x99, 0xb9, 0xc6, 0x2a, 0x21, 0xe6, 0x73, 0xca, 0xe6, 0x4a, 0x51, 0x1a, 0x99, 0x1c, 0x28, 0x04, 0x93, 0xd0, 0x0e, 0xa4, 0xe4, 0xda, 0x5f, 0x50, 0xfe, 0x4a, 0xfe, 0x48, 0xb5, 0xb2, 0xc1, 0xe6, 0x1f, 0x31, 0x7e, 0xef, 0x52, 0x91, 0x43, 0xc3, 0x6e, 0x77, 0xf4, 0x22, 0x6d, 0xbf, 0xe4, 0x63, 0x0e, 0xbf, 0xca, 0x36, 0xeb, 0x5c, 0x84, 0xa5, 0x48, 0x7d, 0x3b, 0x61, 0xa1, 0xdb, 0x5b, 0x2c, 0x71, 0xda, 0x45, 0xc4, 0x28, 0x00, 0x81, 0xdb, 0x31, 0xc9, 0xb4, 0xb2, 0x3b, 0x5d, 0x27, 0xa5, 0x05, 0x1b, 0xc7, 0xdb, 0x10, 0xa9, 0xbd, 0xa6, 0x93, 0x0c, 0x75, 0xe4, 0x39, 0x35, 0x41, 0x3d, 0xc5, 0x06, 0xdb, 0x8e, 0xfd, 0x46, 0x5b, 0x1d, 0x98, 0x95, 0x4f, 0x46, 0xdb, 0xd5, 0xfb, 0x29, 0x5e, 0x9d, 0x0d, 0x32, 0xeb, 0x61, 0x4f, 0xff, 0xd3, 0xf1, 0x46, 0x9a, 0x16, 0x1b, 0x91, 0x71, 0x28, 0xac, 0x4a, 0x14, 0x30, 0x3e, 0x19, 0x54, 0xb9, 0x36, 0xc7, 0x9b, 0x2d, 0xd1, 0x6c, 0x45, 0xe3, 0xdc, 0xde, 0xc8, 0x95, 0x5b, 0x87, 0xf8, 0x41, 0x1d, 0x10, 0x54, 0x01, 0x98, 0x79, 0x25, 0xd1, 0xda, 0xe9, 0xe1, 0xb5, 0x9e, 0xac, 0xeb, 0x42, 0xba, 0x8e, 0xdf, 0x8c, 0x31, 0x21, 0x70, 0xb4, 0x5d, 0xbe, 0xc5, 0x7c, 0x2b, 0xed, 0xe1, 0x94, 0x18, 0xb9, 0x51, 0x3d, 0x03, 0x2c, 0x13, 0x6b, 0xf1, 0x42, 0x6e, 0xe2, 0xb7, 0x12, 0xa0, 0xdd, 0x50, 0x9f, 0x4f, 0x6f, 0xa7, 0x6f, 0xc7, 0x03, 0x61, 0xa0, 0x83, 0xb5, 0xf3, 0x97, 0x98, 0x20, 0x9c, 0x44, 0xea, 0xd0, 0xad, 0x48, 0x64, 0x90, 0x21, 0xd8, 0x9f, 0xa7, 0xa6, 0x44, 0xca, 0x99, 0xc6, 0x36, 0xcb, 0x74, 0x5d, 0x7e, 0x5b, 0xfe, 0x31, 0x6a, 0x31, 0xf3, 0x8c, 0xd0, 0xad, 0x40, 0xa3, 0x1f, 0x7c, 0x44, 0xd6, 0x51, 0xd9, 0xe0, 0x5f, 0x9a, 0x7e, 0x41, 0x9f, 0x40, 0xf3, 0x14, 0xba, 0x85, 0xba, 0x34, 0xba, 0x2d, 0xfb, 0x34, 0xd0, 0xcf, 0x4f, 0xb0, 0xce, 0x6a, 0x51, 0xe9, 0xb0, 0x20, 0xf4, 0xf1, 0x19, 0xb2, 0xc3, 0x90, 0x11, 0x4e, 0x97, 0x55, 0x80, 0x83, 0xc4, 0x17, 0x7e, 0x4c, 0x79, 0x19, 0xfc, 0xd1, 0xe7, 0x78, 0x4b, 0x91, 0x1d, 0xae, 0x92, 0xa6, 0xf6, 0x46, 0x75, 0xe4, 0xad, 0x22, 0x1f, 0xdd, 0xa1, 0x07, 0xb3, 0x1e, 0xfe, 0xd9, 0x92, 0xeb, 0x4b, 0xed, 0xfd, 0x0a, 0xc2, 0x63, 0x27, 0xa4, 0x88, 0x17, 0x60, 0x49, 0x35, 0xdc, 0x8e, 0xa5, 0x7d, 0xab, 0xd3, 0x28, 0x90, 0x50, 0xcd, 0xed, 0x2d, 0xda, 0x15, 0x55, 0x51, 0xf1, 0x1a, 0x0a, 0xf7, 0x39, 0x5d, 0xaa, 0x77, 0x6f, 0x01, 0x8e, 0xa7, 0x7d, 0xfa, 0xff, 0x00, 0x66, 0x10, 0xa8, 0xb8, 0x63, 0x76, 0x90, 0xa8, 0x20, 0x06, 0x56, 0xdb, 0x61, 0xda, 0xbd, 0x4f, 0xcb, 0x24, 0x15, 0x0f, 0xf5, 0x66, 0xe5, 0x5f, 0x4c, 0x53, 0xc3, 0xb7, 0xce, 0x99, 0x6b, 0x17, 0xff, 0xd4, 0xf0, 0xec, 0x57, 0x6f, 0x32, 0xa5, 0xa4, 0x43, 0x76, 0x75, 0xa9, 0xf1, 0x03, 0xfa, 0x64, 0x08, 0x6c, 0x8e, 0xfb, 0x3d, 0x7f, 0xcb, 0x16, 0x2b, 0x3d, 0xbc, 0x16, 0xa3, 0x66, 0x6d, 0x98, 0xfb, 0x1e, 0xb9, 0xac, 0xc8, 0x77, 0xb7, 0x7d, 0x01, 0xb3, 0x37, 0xb8, 0xd3, 0x46, 0x95, 0x68, 0x86, 0xd2, 0x2e, 0x4e, 0xab, 0xf0, 0x23, 0x11, 0x4e, 0x5f, 0xcd, 0x98, 0xe7, 0x25, 0x96, 0x71, 0x83, 0x0f, 0xd6, 0x3c, 0xb9, 0xe7, 0x0d, 0x7c, 0x41, 0x22, 0x5e, 0xb3, 0x20, 0x0c, 0x65, 0x80, 0xc8, 0x63, 0x8e, 0xbb, 0x95, 0xa5, 0x07, 0xeb, 0xcc, 0xac, 0x73, 0x83, 0x4e, 0x5c, 0x59, 0x09, 0xd8, 0xec, 0xc8, 0x57, 0x41, 0xd3, 0x4e, 0x95, 0xa5, 0x5b, 0x4b, 0x6a, 0xcb, 0xab, 0x43, 0x10, 0x4b, 0xeb, 0x85, 0xa2, 0x2c, 0x8e, 0x3f, 0x68, 0x54, 0xf5, 0x00, 0xd3, 0x97, 0x7a, 0x65, 0x79, 0xa6, 0x24, 0x76, 0x6f, 0xd3, 0x62, 0x96, 0x30, 0x78, 0xcb, 0x21, 0xf2, 0xf4, 0x22, 0xce, 0x54, 0x8e, 0x46, 0x26, 0x10, 0x7e, 0x0a, 0xf5, 0xd8, 0xf5, 0x1f, 0x31, 0x98, 0x83, 0x73, 0xb3, 0x91, 0xcd, 0x67, 0xe6, 0x7d, 0xe8, 0x16, 0x69, 0x6f, 0x10, 0x1f, 0x54, 0x9a, 0x37, 0xf5, 0x41, 0x5e, 0x7f, 0x0a, 0x29, 0x62, 0x02, 0xf8, 0x9c, 0xc8, 0x8c, 0x77, 0x6a, 0x99, 0xa0, 0x89, 0xff, 0x00, 0x9c, 0x74, 0xd2, 0xed, 0xed, 0xfc, 0xbb, 0x7b, 0xaa, 0x9a, 0x7d, 0x62, 0xfe, 0x46, 0x2d, 0xfe, 0x4c, 0x51, 0x31, 0x11, 0xa9, 0xf6, 0xef, 0x9b, 0x30, 0x5e, 0x7b, 0x38, 0xdd, 0xf4, 0x7f, 0x95, 0x94, 0xbc, 0x12, 0x43, 0x30, 0x6a, 0xb2, 0xf3, 0x86, 0x40, 0x3e, 0xcb, 0xd7, 0x6a, 0xd7, 0xb1, 0xe9, 0x8f, 0x37, 0x19, 0x97, 0x41, 0x2c, 0x71, 0x20, 0xf5, 0x36, 0x9c, 0x55, 0x78, 0x1d, 0x8a, 0x91, 0xd7, 0x11, 0x14, 0x5a, 0x3e, 0x19, 0x03, 0x10, 0x6b, 0xca, 0xbd, 0x86, 0xf8, 0x9d, 0x95, 0x18, 0x36, 0x65, 0x2e, 0xbc, 0x54, 0x1f, 0xa2, 0x99, 0x00, 0x59, 0x2a, 0x6f, 0x5e, 0x55, 0x15, 0xe9, 0x5f, 0xc3, 0x2f, 0xb6, 0x14, 0xff, 0x00, 0xff, 0xd5, 0xf1, 0x95, 0xfe, 0x80, 0x74, 0x0d, 0x7c, 0xd9, 0x89, 0x3d, 0x78, 0x57, 0x8b, 0xc5, 0x28, 0xe8, 0x55, 0xf7, 0x1f, 0x48, 0xca, 0x38, 0xb8, 0x83, 0x9f, 0x93, 0x07, 0x85, 0x3a, 0x7a, 0x6f, 0x95, 0x66, 0x2b, 0x2c, 0x4c, 0x0d, 0x14, 0x00, 0x3e, 0x9c, 0xc3, 0x98, 0x76, 0xb8, 0x45, 0xbd, 0x02, 0xde, 0x48, 0xee, 0xdc, 0xa0, 0x15, 0xe2, 0x2b, 0xc8, 0x8a, 0x8a, 0xfd, 0x3b, 0x66, 0x3f, 0x00, 0x73, 0x84, 0x2d, 0x36, 0xb5, 0xb5, 0x9e, 0x35, 0x1c, 0x29, 0xc4, 0xfe, 0xc8, 0x04, 0x7f, 0xc4, 0x69, 0x91, 0xe1, 0x67, 0x2c, 0x4a, 0xd2, 0xe9, 0x4e, 0xe3, 0xd4, 0xf4, 0x81, 0x5a, 0x12, 0xc5, 0x41, 0x3f, 0x79, 0x38, 0x9b, 0x60, 0x20, 0x07, 0x34, 0xb0, 0xc9, 0x03, 0x5c, 0x23, 0x03, 0x53, 0x13, 0x56, 0x88, 0xdf, 0x09, 0xda, 0x9b, 0xd3, 0xb6, 0x52, 0x0e, 0xec, 0xe4, 0x29, 0x24, 0xfc, 0xd0, 0xe7, 0x75, 0xe5, 0x57, 0x6b, 0x61, 0xfb, 0xf0, 0xca, 0xaa, 0x57, 0xa8, 0xe6, 0x78, 0x1a, 0x7d, 0xf9, 0x95, 0x8a, 0x5e, 0xa0, 0xe3, 0x67, 0x8f, 0xa0, 0xbd, 0x5b, 0xf2, 0xdf, 0x4a, 0x82, 0xcb, 0x4a, 0xb3, 0xb0, 0xb4, 0x41, 0x0a, 0x70, 0x48, 0xd9, 0x57, 0x60, 0x51, 0x3a, 0x8f, 0xbc, 0xe6, 0x7b, 0xcb, 0xe4, 0x3b, 0xa7, 0x3f, 0x9b, 0x9f, 0x9a, 0xba, 0x77, 0xe5, 0x5f, 0x95, 0x9c, 0x59, 0x94, 0x9f, 0xcd, 0x37, 0x8c, 0xa9, 0xa6, 0xd9, 0x39, 0xaa, 0xd0, 0x7d, 0xa9, 0x1c, 0x03, 0x5e, 0x09, 0xff, 0x00, 0x0c, 0x76, 0xcb, 0x62, 0x2d, 0xa5, 0xf2, 0x85, 0xbf, 0xe7, 0x87, 0xe6, 0xa3, 0x5e, 0x4d, 0xa8, 0xc9, 0xe6, 0x8b, 0xd5, 0x69, 0x5c, 0xb0, 0x4a, 0xab, 0xc4, 0xb5, 0x35, 0x0a, 0xaa, 0xea, 0x40, 0x03, 0xa0, 0xf6, 0xcb, 0x40, 0x4d, 0x3e, 0xdb, 0xff, 0x00, 0x9c, 0x7f, 0xfc, 0xce, 0x4f, 0xcc, 0xbf, 0x26, 0x25, 0xe5, 0xd3, 0x2f, 0xe9, 0xdd, 0x3d, 0xfe, 0xab, 0xa9, 0xaa, 0xd2, 0xa6, 0x40, 0x2a, 0xb2, 0x71, 0x00, 0x01, 0xea, 0x0d, 0xe8, 0x3a, 0x64, 0x25, 0x16, 0x1c, 0x8b, 0xd9, 0x51, 0x39, 0x28, 0x12, 0x51, 0x41, 0xfd, 0xa3, 0xd2, 0xb9, 0x4f, 0x0d, 0x33, 0xb5, 0xf4, 0x87, 0x9d, 0x79, 0x0e, 0xb4, 0xaf, 0x6a, 0xf8, 0xf1, 0xf0, 0xc9, 0xda, 0xbf, 0xff, 0xd6, 0xf2, 0xc6, 0xb5, 0x68, 0x64, 0xd0, 0x6d, 0x35, 0x20, 0x39, 0xcd, 0x13, 0x0f, 0x5e, 0x61, 0xfc, 0x8f, 0x40, 0x8b, 0x5e, 0xe0, 0x66, 0x1c, 0x4f, 0xaa, 0x9d, 0xe6, 0xa6, 0x1e, 0x91, 0x2e, 0xa9, 0x87, 0x95, 0xee, 0x9c, 0xc5, 0x55, 0x34, 0x60, 0x40, 0xae, 0x57, 0x30, 0xd9, 0xa7, 0x95, 0xbd, 0x6f, 0xcb, 0x26, 0x39, 0x40, 0x0d, 0x4e, 0xc0, 0x9f, 0x9e, 0x50, 0x5d, 0xac, 0x79, 0x33, 0x8b, 0xbb, 0x9b, 0x3b, 0x6b, 0x35, 0x48, 0x54, 0x09, 0x29, 0x56, 0x7f, 0xe1, 0x86, 0x72, 0x00, 0x2c, 0x6e, 0xf7, 0x63, 0x3e, 0x63, 0xbd, 0xbd, 0x5d, 0x20, 0x2a, 0xb3, 0xa4, 0x33, 0x48, 0xab, 0x21, 0x43, 0xf1, 0x2c, 0x47, 0xed, 0x1d, 0xbc, 0x73, 0x18, 0x9b, 0x64, 0x28, 0x96, 0x3a, 0xc7, 0x49, 0xb0, 0xf4, 0xcc, 0xe9, 0x73, 0x6c, 0xb4, 0xf8, 0x67, 0x92, 0x32, 0x21, 0x70, 0x7b, 0x89, 0x05, 0x57, 0xef, 0x38, 0x28, 0x94, 0x4a, 0x7d, 0x13, 0x7d, 0x6a, 0xd3, 0x4c, 0xb8, 0xf2, 0xc3, 0xc8, 0x2e, 0x03, 0xf3, 0xe2, 0x7d, 0x33, 0xb7, 0xc5, 0xcc, 0x71, 0x03, 0xc6, 0xb9, 0x64, 0x06, 0xe2, 0x9a, 0xf2, 0x4f, 0xd2, 0x6d, 0xe9, 0xfe, 0x41, 0x45, 0x5b, 0x18, 0x66, 0xa5, 0x64, 0x09, 0xf4, 0xd5, 0xb7, 0xcd, 0x93, 0xc7, 0xcf, 0x9b, 0xe5, 0x6f, 0xf9, 0xc8, 0x0d, 0x56, 0xeb, 0x59, 0xfc, 0xce, 0xd5, 0x12, 0x61, 0xc4, 0x69, 0xe9, 0x0d, 0xa4, 0x4b, 0xfe, 0x48, 0x40, 0xd5, 0x3e, 0xe4, 0xb6, 0x64, 0x8e, 0x4c, 0x02, 0x61, 0x65, 0xa0, 0x14, 0xb4, 0xb6, 0xb0, 0xb1, 0xb6, 0xb2, 0x97, 0xcb, 0xf1, 0x5a, 0x2d, 0xc6, 0xa5, 0xac, 0xb4, 0x70, 0x5d, 0xc7, 0x3d, 0xc1, 0x51, 0x24, 0x91, 0xc9, 0x31, 0x75, 0x6b, 0x70, 0x9f, 0x14, 0x68, 0x01, 0x46, 0xe4, 0xb5, 0xa3, 0x17, 0xcb, 0x40, 0x61, 0x6f, 0x47, 0xff, 0x00, 0x9c, 0x3a, 0x8f, 0x5b, 0x4f, 0x3c, 0x6b, 0xb7, 0xfa, 0x30, 0x91, 0x3c, 0xa4, 0xb1, 0x95, 0xb9, 0x82, 0x42, 0x0a, 0xbc, 0x8e, 0xe4, 0xdb, 0xa9, 0xef, 0xc9, 0x17, 0x91, 0x24, 0x7c, 0xb2, 0x05, 0x64, 0xfb, 0x75, 0x64, 0x32, 0x39, 0x69, 0x5b, 0x9c, 0xad, 0xb9, 0xdb, 0xa7, 0xb5, 0x3b, 0x53, 0x2a, 0x21, 0x41, 0x44, 0xf3, 0x8b, 0x8f, 0x2e, 0x43, 0x9d, 0x2b, 0xd4, 0x57, 0x23, 0x41, 0x36, 0xff, 0x00, 0xff, 0xd7, 0xf0, 0xc0, 0xd5, 0xb5, 0x11, 0x64, 0xb6, 0x3f, 0x59, 0x90, 0xd9, 0xab, 0x06, 0xf4, 0x79, 0x7c, 0x3b, 0x74, 0xc8, 0x08, 0x8b, 0xb6, 0xe3, 0x96, 0x55, 0x57, 0xb3, 0x3e, 0xf2, 0x35, 0xc7, 0xd6, 0x0b, 0x45, 0x5d, 0xdc, 0x8a, 0x7d, 0xd9, 0x8d, 0x94, 0x3b, 0x3d, 0x1c, 0x9e, 0xc3, 0xe5, 0xc3, 0x2c, 0x7c, 0xc5, 0x0f, 0xee, 0xdb, 0x8b, 0x0c, 0xc4, 0x26, 0x9d, 0xa0, 0x9a, 0x7d, 0x2c, 0xe5, 0xe4, 0x55, 0x7f, 0xee, 0xc1, 0x15, 0x04, 0xd0, 0x12, 0x3c, 0x72, 0x89, 0x1b, 0x2c, 0xcc, 0xa8, 0x2a, 0x8b, 0x87, 0xbb, 0x63, 0x1a, 0x28, 0x65, 0xf0, 0xed, 0xf2, 0xc3, 0xc2, 0x0a, 0x06, 0x4a, 0x46, 0xc7, 0xa5, 0xa3, 0x59, 0xc8, 0xb2, 0xc7, 0x45, 0x22, 0x9c, 0x14, 0x54, 0x10, 0x46, 0xf5, 0x1d, 0x32, 0x5c, 0x14, 0x14, 0xe4, 0x32, 0x2f, 0x3a, 0xf3, 0xb6, 0x90, 0x9a, 0x6d, 0xae, 0x9f, 0x3d, 0xab, 0xb8, 0x8a, 0x3b, 0xf8, 0x39, 0x44, 0x58, 0xf0, 0x08, 0xd5, 0x14, 0xa5, 0x7b, 0x65, 0x98, 0x8e, 0xfb, 0xb5, 0x67, 0x87, 0xa5, 0xef, 0x5e, 0x44, 0x96, 0x35, 0xb5, 0xb6, 0x59, 0x36, 0xfd, 0xd8, 0xa0, 0xf1, 0x20, 0x53, 0x33, 0xc0, 0x79, 0x59, 0x73, 0x7c, 0xd7, 0xf9, 0xfb, 0xa2, 0xcd, 0x67, 0xf9, 0xa7, 0x7b, 0x72, 0xf1, 0x71, 0x83, 0x53, 0x86, 0x0b, 0x98, 0x24, 0x22, 0x8a, 0xcc, 0x88, 0x23, 0x7f, 0xb8, 0xae, 0xf9, 0x7c, 0x50, 0x1e, 0x5f, 0x7c, 0x48, 0x21, 0x44, 0x6b, 0xce, 0x9b, 0xb0, 0x1b, 0x9e, 0xf5, 0xaf, 0x8e, 0x4d, 0x5f, 0x7a, 0x7f, 0xce, 0x34, 0xf9, 0x5d, 0x3c, 0xa3, 0xf9, 0x69, 0x63, 0xa9, 0x3c, 0x27, 0xeb, 0xda, 0xe1, 0x37, 0xd7, 0x2e, 0xaa, 0xdb, 0x06, 0xda, 0x30, 0x49, 0xfe, 0x54, 0x03, 0x03, 0x49, 0xdc, 0xb3, 0xaf, 0x38, 0xfe, 0x6a, 0xf9, 0x47, 0xc9, 0x3a, 0x74, 0x97, 0xfa, 0xf6, 0xaf, 0x15, 0x85, 0xb8, 0x75, 0x89, 0xb8, 0x87, 0x9a, 0x72, 0xee, 0x2a, 0x14, 0x24, 0x60, 0xb1, 0xa8, 0xdf, 0x07, 0x0b, 0x2d, 0xcb, 0xcf, 0x7f, 0xe8, 0x6a, 0xff, 0x00, 0x26, 0xbd, 0x6a, 0x7f, 0x89, 0x2f, 0xf8, 0x52, 0x9e, 0xb7, 0xe8, 0xb9, 0xb8, 0x57, 0xc2, 0x95, 0xe9, 0x8f, 0x08, 0x5a, 0x2f, 0xff, 0xd0, 0xf0, 0x4d, 0x40, 0xaa, 0xd7, 0x00, 0x64, 0xcb, 0x3c, 0x97, 0xa8, 0xb5, 0x9e, 0xa3, 0x1a, 0xd6, 0x84, 0x95, 0x3f, 0x45, 0x72, 0x9c, 0xa2, 0xc3, 0x99, 0xa5, 0x9d, 0x49, 0xf4, 0x17, 0x97, 0xaf, 0x63, 0x17, 0x52, 0x6f, 0xf0, 0xc8, 0x43, 0x6f, 0x9a, 0xe9, 0x07, 0x70, 0x0e, 0xec, 0x83, 0x51, 0x44, 0xb8, 0x61, 0x1a, 0x9e, 0x11, 0xd3, 0x91, 0x60, 0x68, 0x6b, 0xd3, 0x31, 0x4f, 0x36, 0xd3, 0x4c, 0x52, 0xef, 0x4c, 0xd5, 0x0c, 0xc4, 0x69, 0xda, 0x94, 0xc8, 0x3a, 0xf0, 0x66, 0x07, 0x73, 0xe0, 0x40, 0xfd, 0x79, 0x93, 0x12, 0x1c, 0x9c, 0x32, 0xc7, 0xfc, 0x41, 0x33, 0xd2, 0xb4, 0x6f, 0x38, 0x98, 0x65, 0x76, 0xbf, 0x69, 0x42, 0xd0, 0xaa, 0xc9, 0xde, 0x95, 0xad, 0x28, 0x46, 0x4e, 0xac, 0x39, 0x77, 0x80, 0x11, 0xbf, 0xd8, 0xc7, 0x7c, 0xe1, 0xa5, 0xf9, 0x92, 0x4d, 0x32, 0x5b, 0x8b, 0x93, 0x27, 0xa7, 0x68, 0x56, 0xe2, 0x45, 0xda, 0x85, 0x61, 0x6e, 0x67, 0xad, 0x6b, 0xb0, 0x38, 0xc2, 0x81, 0xe4, 0xc7, 0x52, 0x31, 0x1c, 0x67, 0x86, 0x5b, 0xbd, 0x37, 0xca, 0x7a, 0x94, 0xb1, 0x69, 0xb6, 0x2e, 0xb7, 0x15, 0x48, 0xc2, 0xb4, 0x52, 0x53, 0xac, 0x32, 0xaf, 0xb1, 0xed, 0x9b, 0x10, 0x36, 0x78, 0x5c, 0x9f, 0x51, 0x64, 0x1f, 0x98, 0x3e, 0x58, 0xb6, 0xfc, 0xc8, 0xf2, 0xe5, 0xbc, 0x68, 0x52, 0x2d, 0x5a, 0xd1, 0x84, 0xb6, 0xf3, 0x95, 0x0e, 0xc0, 0x85, 0xe2, 0xcb, 0xd8, 0xd1, 0xbb, 0xe4, 0xc1, 0xa6, 0x97, 0xce, 0x17, 0x5f, 0x95, 0xde, 0x6d, 0xb6, 0xbe, 0xb7, 0x69, 0x34, 0xf3, 0x3c, 0x72, 0xcf, 0xe8, 0xa3, 0x45, 0x49, 0x95, 0x4a, 0x90, 0x3e, 0x35, 0x5a, 0x95, 0x1d, 0xfe, 0x21, 0x93, 0x4d, 0xbe, 0xd2, 0xd2, 0xf5, 0x8b, 0xbd, 0x32, 0x2d, 0x3f, 0x4c, 0x9a, 0xe4, 0xca, 0x9e, 0x90, 0x85, 0x65, 0x55, 0x08, 0x85, 0x91, 0x01, 0x3b, 0x0a, 0x05, 0xe9, 0xb0, 0xc0, 0x5a, 0xc3, 0xcd, 0x3f, 0x3b, 0x7f, 0x26, 0xec, 0xff, 0x00, 0x35, 0x6d, 0x6d, 0xb5, 0x3d, 0x16, 0xfe, 0x0d, 0x3b, 0xcd, 0x96, 0x01, 0x92, 0x46, 0x9e, 0xa2, 0x0b, 0xc8, 0xb7, 0x28, 0x92, 0x71, 0xfb, 0x2e, 0xa7, 0xec, 0x3d, 0x0f, 0xc2, 0x68, 0x71, 0x05, 0x95, 0xd3, 0xe7, 0x9f, 0xfa, 0x16, 0x2f, 0xcd, 0x7f, 0x43, 0xd6, 0xfa, 0xa5, 0x97, 0xab, 0xeb, 0x7a, 0x5f, 0x55, 0xfa, 0xec, 0x5e, 0xaf, 0x0f, 0xf7, 0xed, 0x2b, 0x4e, 0x15, 0xff, 0x00, 0x65, 0xdf, 0x8e, 0x14, 0xf1, 0xbf, 0xff, 0xd1, 0xf0, 0x5a, 0xa7, 0x18, 0x5e, 0x56, 0x1f, 0x68, 0x71, 0x5f, 0xa7, 0xbe, 0x2a, 0x98, 0xdb, 0xfa, 0x90, 0x24, 0x37, 0xb0, 0xfd, 0xb8, 0xa8, 0x58, 0x78, 0xae, 0x43, 0xc9, 0xb4, 0x6d, 0xbb, 0xda, 0x3c, 0xa1, 0xad, 0x43, 0xa8, 0xda, 0xc5, 0x2a, 0x3d, 0x26, 0x5a, 0x02, 0x2b, 0xbe, 0x60, 0x64, 0x8d, 0x17, 0x6f, 0x8b, 0x20, 0x90, 0x7a, 0x3c, 0x32, 0x8b, 0xa8, 0x02, 0xf3, 0xfd, 0xe0, 0x1b, 0x11, 0x98, 0x66, 0x3b, 0xb9, 0x62, 0x54, 0x83, 0x36, 0xf2, 0xa4, 0xe4, 0x29, 0x34, 0xeb, 0xc8, 0x74, 0xae, 0x0d, 0xc3, 0x65, 0x82, 0x13, 0x6b, 0x57, 0xba, 0x54, 0xe4, 0x8c, 0x41, 0x1b, 0x75, 0xa7, 0xe0, 0x72, 0x5c, 0x4c, 0x84, 0x50, 0x5a, 0xb3, 0xdd, 0xdd, 0xc3, 0x24, 0x33, 0xb1, 0x60, 0xe0, 0x86, 0x52, 0x45, 0x38, 0xd2, 0x87, 0x24, 0x26, 0x6d, 0x8c, 0xe1, 0x41, 0x25, 0xfc, 0xa3, 0xd7, 0x2f, 0x6f, 0x3c, 0xbf, 0x73, 0xa5, 0xb2, 0x2c, 0xd1, 0x69, 0x17, 0x2f, 0x6b, 0x14, 0x8c, 0x0f, 0x21, 0x0d, 0x79, 0x46, 0x09, 0x15, 0xed, 0xb7, 0x4e, 0xd9, 0xb9, 0x8b, 0xcb, 0xe4, 0xa2, 0x5e, 0xa3, 0xa6, 0xdf, 0x6a, 0x36, 0xe4, 0xcd, 0x69, 0x1c, 0x4e, 0x84, 0x7c, 0x76, 0xab, 0x21, 0x67, 0xa8, 0xa7, 0xd9, 0xf8, 0x4d, 0x2b, 0xf3, 0xc3, 0x4d, 0x49, 0x57, 0x98, 0x75, 0x6f, 0x31, 0xda, 0xf9, 0xa3, 0x4b, 0xfd, 0x1f, 0x69, 0x1d, 0xae, 0xa1, 0xa9, 0x7e, 0xee, 0xe6, 0xd2, 0x79, 0x18, 0xf3, 0xb5, 0x1f, 0xee, 0xd9, 0x0a, 0x01, 0x4e, 0x3f, 0xb3, 0x4d, 0xf2, 0x9c, 0xb9, 0x04, 0x05, 0xb7, 0xe2, 0x87, 0x1e, 0xdd, 0x19, 0x3e, 0xaf, 0x6b, 0xae, 0xcb, 0x6d, 0x13, 0x0d, 0x45, 0xa2, 0x8e, 0x06, 0xe5, 0x13, 0x2a, 0x02, 0x01, 0x5e, 0x82, 0xb5, 0x04, 0xe6, 0x11, 0xd4, 0xcd, 0xda, 0x43, 0x49, 0x8e, 0xb7, 0xdc, 0xb1, 0x51, 0xe6, 0x4d, 0x76, 0xd2, 0x61, 0x15, 0xaa, 0x4b, 0xa8, 0xc9, 0x6e, 0x49, 0x79, 0x20, 0xe6, 0x8c, 0x49, 0xad, 0x43, 0x16, 0xe4, 0xa7, 0xaf, 0x43, 0xd3, 0x26, 0x35, 0x75, 0xcd, 0xa8, 0xe8, 0x87, 0x46, 0xbf, 0xc7, 0x9a, 0xff, 0x00, 0xd6, 0xbf, 0x48, 0xfe, 0x88, 0xfd, 0xe7, 0x0f, 0xab, 0xfa, 0x3f, 0x58, 0x7f, 0x5f, 0x8d, 0x3f, 0x9f, 0xa7, 0x5e, 0xd4, 0xc3, 0xf9, 0xd1, 0x7c, 0xb6, 0x47, 0xe4, 0x3a, 0x5b, 0xff, 0xd2, 0xf0, 0xb7, 0xa6, 0x1e, 0xdf, 0xd3, 0xf6, 0xa5, 0x71, 0x54, 0xdb, 0x4b, 0x80, 0x3c, 0x42, 0x26, 0xee, 0x29, 0xbe, 0x51, 0x23, 0x4e, 0x44, 0x05, 0x84, 0x45, 0xa5, 0xd5, 0xf7, 0x97, 0x2e, 0xfd, 0x6b, 0x6a, 0x98, 0x09, 0xab, 0xc7, 0xfc, 0x46, 0x3b, 0x4c, 0x26, 0x32, 0x30, 0x3e, 0x4f, 0x49, 0xd0, 0xfc, 0xfb, 0x05, 0xd4, 0x4a, 0x7d, 0x40, 0xac, 0x3a, 0x8e, 0x84, 0x1c, 0xc5, 0x96, 0x2a, 0x73, 0xe1, 0x9c, 0x16, 0x6d, 0xa5, 0x79, 0x86, 0xd6, 0xec, 0x80, 0x5a, 0xa0, 0xf5, 0xca, 0xcc, 0x5c, 0xa1, 0x2b, 0x1b, 0x26, 0x30, 0x6a, 0x31, 0x46, 0xcf, 0x1c, 0x87, 0x94, 0x64, 0x9e, 0x3d, 0xb6, 0xf0, 0xca, 0xa8, 0x39, 0x51, 0x99, 0x42, 0x6b, 0x1a, 0xc5, 0xa5, 0xa5, 0x94, 0xf7, 0x92, 0xc8, 0xaa, 0xb1, 0x23, 0x30, 0x04, 0xf8, 0x0e, 0x9f, 0x4e, 0x4a, 0x11, 0xb2, 0xd5, 0x9b, 0x25, 0x06, 0x1b, 0xff, 0x00, 0x38, 0xfd, 0xad, 0xdf, 0xda, 0xf9, 0xa2, 0xfe, 0xc5, 0x42, 0xbe, 0x9b, 0x7f, 0x0b, 0xdd, 0xdd, 0x07, 0xaf, 0x14, 0x68, 0xd8, 0x71, 0x6d, 0xbb, 0x90, 0xfc, 0x73, 0x6e, 0xf2, 0xf2, 0xdd, 0xf4, 0xad, 0xa6, 0xab, 0x6d, 0x69, 0x14, 0xfa, 0xee, 0xa0, 0xe2, 0x0b, 0x0d, 0x39, 0x19, 0xfe, 0x11, 0xc5, 0x1a, 0x4a, 0x1d, 0x8f, 0x73, 0x4f, 0xf8, 0x96, 0x0b, 0x40, 0x8d, 0xec, 0xf3, 0x6d, 0x3f, 0x52, 0xba, 0xd6, 0x35, 0x8b, 0xbf, 0x36, 0x6a, 0x5f, 0x0d, 0xc5, 0xdc, 0xa8, 0xb6, 0xa8, 0x7a, 0xc5, 0x6c, 0x9b, 0x22, 0x0f, 0xa3, 0x73, 0x9a, 0xbc, 0xb3, 0xe2, 0x36, 0xed, 0xb1, 0x43, 0x80, 0x53, 0xd0, 0xa7, 0xd4, 0x44, 0xfa, 0x7a, 0xda, 0x83, 0xbd, 0x3e, 0x2f, 0xa7, 0x2b, 0xad, 0x9b, 0xb8, 0x8d, 0xa8, 0xe8, 0x91, 0xdb, 0xfa, 0x2d, 0x6f, 0xc3, 0x8a, 0x2d, 0x56, 0xa3, 0xad, 0x4f, 0x5c, 0xa4, 0x0d, 0xdc, 0xa3, 0xca, 0xd0, 0xbf, 0xa1, 0xe3, 0xfa, 0xe7, 0x0f, 0xf2, 0xb9, 0x57, 0xbf, 0x1a, 0xe4, 0xb8, 0x57, 0xc5, 0xdd, 0xff, 0xd3, 0xf0, 0xcc, 0x5d, 0x7b, 0x70, 0xc5, 0x53, 0x6d, 0x2f, 0xd5, 0xe4, 0x69, 0xfd, 0xdf, 0xec, 0xd7, 0xad, 0x7d, 0xb2, 0x8c, 0x8d, 0xd8, 0xed, 0x91, 0x9f, 0x43, 0xea, 0xe7, 0xeb, 0x94, 0xad, 0x3e, 0x1e, 0x95, 0xfc, 0x72, 0x81, 0x7d, 0x1c, 0x9d, 0xba, 0xb1, 0x7b, 0xdf, 0xa9, 0x7a, 0xdf, 0xee, 0x2f, 0xd4, 0xfa, 0xe7, 0xed, 0x7a, 0x7f, 0xdd, 0xff, 0x00, 0xb2, 0xae, 0x64, 0x0b, 0xea, 0xe3, 0x9a, 0xbf, 0x4a, 0x6f, 0xa4, 0xff, 0x00, 0x89, 0xbd, 0x45, 0xfa, 0xb5, 0x79, 0xf7, 0xeb, 0xc7, 0xe9, 0xae, 0x57, 0x2e, 0x17, 0x23, 0x1f, 0x89, 0xd1, 0x99, 0x8f, 0xf1, 0xa7, 0x11, 0xcf, 0xd3, 0xf5, 0x29, 0xb5, 0x6b, 0xd3, 0xe8, 0xcc, 0x7f, 0x45, 0xb9, 0xa3, 0xc5, 0x62, 0xbe, 0x68, 0xff, 0x00, 0x15, 0xfd, 0x4c, 0xfe, 0x90, 0xaf, 0xd4, 0xab, 0xf1, 0x7a, 0x7f, 0x62, 0x9d, 0xab, 0xdf, 0x32, 0xb1, 0x70, 0x5e, 0xdc, 0xdc, 0x2d, 0x47, 0x8b, 0x5e, 0xae, 0x4c, 0xbf, 0xf2, 0x37, 0x9f, 0x3d, 0x5b, 0xd2, 0xff, 0x00, 0x8e, 0x87, 0xee, 0x29, 0x5a, 0xf2, 0xf4, 0xaa, 0xd4, 0xa5, 0x36, 0xa7, 0x3a, 0x57, 0xfd, 0x8e, 0x64, 0x3a, 0xf2, 0xf6, 0xbf, 0xcc, 0x7f, 0x5b, 0xfc, 0x23, 0xa7, 0xfe, 0x8e, 0xff, 0x00, 0x8e, 0x37, 0xd6, 0x63, 0xfa, 0xe5, 0x2b, 0xcb, 0x87, 0xec, 0xd6, 0xbd, 0xb9, 0x7d, 0xac, 0xc7, 0xcd, 0x7c, 0x2d, 0xf8, 0x2b, 0x89, 0x26, 0x8f, 0xd4, 0xfa, 0x94, 0x3e, 0x85, 0x29, 0xc9, 0x69, 0xfc, 0x33, 0x58, 0x5d, 0x9c, 0x79, 0xb2, 0xbb, 0x0f, 0xac, 0x7a, 0x2b, 0xea, 0x75, 0xef, 0x92, 0x0c, 0x53, 0x3d, 0x2f, 0xd4, 0xfa, 0xbb, 0xfa, 0x74, 0xf5, 0x39, 0x9a, 0xd7, 0xe7, 0x80, 0x53, 0x79, 0xba, 0x5b, 0xfe, 0x97, 0xfa, 0x4b, 0xfc, 0xba, 0x7f, 0xb1, 0xc7, 0xab, 0x1e, 0x8f, 0xff, 0xd9
+};
diff --git a/talk/base/transformadapter.cc b/talk/base/transformadapter.cc
new file mode 100644
index 0000000..53a55a8
--- /dev/null
+++ b/talk/base/transformadapter.cc
@@ -0,0 +1,202 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#include "talk/base/transformadapter.h"
+
+#include <cstring>
+
+#include "talk/base/common.h"
+
+namespace talk_base {
+
+///////////////////////////////////////////////////////////////////////////////
+
+TransformAdapter::TransformAdapter(StreamInterface * stream,
+                                   TransformInterface * transform,
+                                   bool direction_read)
+    : StreamAdapterInterface(stream), transform_(transform),
+      direction_read_(direction_read), state_(ST_PROCESSING), len_(0) {
+}
+
+TransformAdapter::~TransformAdapter() {
+  TransformAdapter::Close();
+  delete transform_;
+}
+
+StreamResult
+TransformAdapter::Read(void * buffer, size_t buffer_len,
+                       size_t * read, int * error) {
+  if (!direction_read_)
+    return SR_EOS;
+
+  while (state_ != ST_ERROR) {
+    if (state_ == ST_COMPLETE)
+      return SR_EOS;
+
+    // Buffer more data
+    if ((state_ == ST_PROCESSING) && (len_ < sizeof(buffer_))) {
+      size_t subread;
+      StreamResult result = StreamAdapterInterface::Read(
+                              buffer_ + len_,
+                              sizeof(buffer_) - len_,
+                              &subread,
+                              &error_);
+      if (result == SR_BLOCK) {
+        return SR_BLOCK;
+      } else if (result == SR_ERROR) {
+        state_ = ST_ERROR;
+        break;
+      } else if (result == SR_EOS) {
+        state_ = ST_FLUSHING;
+      } else {
+        len_ += subread;
+      }
+    }
+
+    // Process buffered data
+    size_t in_len = len_;
+    size_t out_len = buffer_len;
+    StreamResult result = transform_->Transform(buffer_, &in_len,
+                                                buffer, &out_len,
+                                                (state_ == ST_FLUSHING));
+    ASSERT(result != SR_BLOCK);
+    if (result == SR_EOS) {
+      // Note: Don't signal SR_EOS this iteration, unless out_len is zero
+      state_ = ST_COMPLETE;
+    } else if (result == SR_ERROR) {
+      state_ = ST_ERROR;
+      error_ = -1; // TODO: propagate error
+      break;
+    } else if ((out_len == 0) && (state_ == ST_FLUSHING)) {
+      // If there is no output AND no more input, then something is wrong
+      state_ = ST_ERROR;
+      error_ = -1; // TODO: better error code?
+      break;
+    }
+
+    len_ -= in_len;
+    if (len_ > 0)
+      memmove(buffer_, buffer_ + in_len, len_);
+
+    if (out_len == 0)
+      continue;
+
+    if (read)
+      *read = out_len;
+    return SR_SUCCESS;
+  }
+
+  if (error)
+    *error = error_;
+  return SR_ERROR;
+}
+
+StreamResult
+TransformAdapter::Write(const void * data, size_t data_len,
+                        size_t * written, int * error) {
+  if (direction_read_)
+    return SR_EOS;
+
+  size_t bytes_written = 0;
+  while (state_ != ST_ERROR) {
+    if (state_ == ST_COMPLETE)
+      return SR_EOS;
+
+    if (len_ < sizeof(buffer_)) {
+      // Process buffered data
+      size_t in_len = data_len;
+      size_t out_len = sizeof(buffer_) - len_;
+      StreamResult result = transform_->Transform(data, &in_len,
+                                                  buffer_ + len_, &out_len,
+                                                  (state_ == ST_FLUSHING));
+
+      ASSERT(result != SR_BLOCK);
+      if (result == SR_EOS) {
+        // Note: Don't signal SR_EOS this iteration, unless no data written
+        state_ = ST_COMPLETE;
+      } else if (result == SR_ERROR) {
+        ASSERT(false); // When this happens, think about what should be done
+        state_ = ST_ERROR;
+        error_ = -1; // TODO: propagate error
+        break;
+      }
+
+      len_ = out_len;
+      bytes_written = in_len;
+    }
+
+    size_t pos = 0;
+    while (pos < len_) {
+      size_t subwritten;
+      StreamResult result = StreamAdapterInterface::Write(buffer_ + pos,
+                                                          len_ - pos,
+                                                          &subwritten,
+                                                          &error_);
+      if (result == SR_BLOCK) {
+        ASSERT(false); // TODO: we should handle this
+        return SR_BLOCK;
+      } else if (result == SR_ERROR) {
+        state_ = ST_ERROR;
+        break;
+      } else if (result == SR_EOS) {
+        state_ = ST_COMPLETE;
+        break;
+      }
+
+      pos += subwritten;
+    }
+
+    len_ -= pos;
+    if (len_ > 0)
+      memmove(buffer_, buffer_ + pos, len_);
+
+    if (bytes_written == 0)
+      continue;
+
+    if (written)
+      *written = bytes_written;
+    return SR_SUCCESS;
+  }
+
+  if (error)
+    *error = error_;
+  return SR_ERROR;
+}
+
+void
+TransformAdapter::Close() {
+  if (!direction_read_ && (state_ == ST_PROCESSING)) {
+    state_ = ST_FLUSHING;
+    do {
+      Write(0, 0, NULL, NULL);
+    } while (state_ == ST_FLUSHING);
+  }
+  state_ = ST_COMPLETE;
+  StreamAdapterInterface::Close();
+}
+
+} // namespace talk_base
diff --git a/talk/base/transformadapter.h b/talk/base/transformadapter.h
new file mode 100644
index 0000000..e96a13d
--- /dev/null
+++ b/talk/base/transformadapter.h
@@ -0,0 +1,97 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#ifndef TALK_BASE_TRANSFORMADAPTER_H__
+#define TALK_BASE_TRANSFORMADAPTER_H__
+
+#include "talk/base/stream.h"
+
+namespace talk_base {
+///////////////////////////////////////////////////////////////////////////////
+
+class TransformInterface {
+public:
+  virtual ~TransformInterface() { }
+
+  // Transform should convert the in_len bytes of input into the out_len-sized
+  // output buffer.  If flush is true, there will be no more data following
+  // input.
+  // After the transformation, in_len contains the number of bytes consumed, and
+  // out_len contains the number of bytes ready in output.
+  // Note: Transform should not return SR_BLOCK, as there is no asynchronous
+  // notification available.
+  virtual StreamResult Transform(const void * input, size_t * in_len,
+                                 void * output, size_t * out_len,
+                                 bool flush) = 0;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+// TransformAdapter causes all data passed through to be transformed by the
+// supplied TransformInterface object, which may apply compression, encryption,
+// etc.
+
+class TransformAdapter : public StreamAdapterInterface {
+public:
+  // Note that the transformation is unidirectional, in the direction specified
+  // by the constructor.  Operations in the opposite direction result in SR_EOS.
+  TransformAdapter(StreamInterface * stream,
+                   TransformInterface * transform,
+                   bool direction_read);
+  virtual ~TransformAdapter();
+  
+  virtual StreamResult Read(void * buffer, size_t buffer_len,
+                            size_t * read, int * error);
+  virtual StreamResult Write(const void * data, size_t data_len,
+                             size_t * written, int * error);
+  virtual void Close();
+
+  // Apriori, we can't tell what the transformation does to the stream length.
+  virtual bool GetAvailable(size_t* size) const { return false; }
+  virtual bool ReserveSize(size_t size) { return true; }
+
+  // Transformations might not be restartable
+  virtual bool Rewind() { return false; }
+
+private:
+  enum State { ST_PROCESSING, ST_FLUSHING, ST_COMPLETE, ST_ERROR };
+  enum { BUFFER_SIZE = 1024 };
+
+  TransformInterface * transform_;
+  bool direction_read_;
+  State state_;
+  int error_;
+
+  char buffer_[BUFFER_SIZE];
+  size_t len_;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+} // namespace talk_base
+
+#endif // TALK_BASE_TRANSFORMADAPTER_H__
diff --git a/talk/base/win32_unittest.cc b/talk/base/win32_unittest.cc
new file mode 100644
index 0000000..4800e6e
--- /dev/null
+++ b/talk/base/win32_unittest.cc
@@ -0,0 +1,54 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#include <string>
+
+#include "talk/base/gunit.h"
+#include "talk/base/win32.h"
+
+#ifndef WIN32
+#error Only for Windows
+#endif
+
+namespace talk_base {
+
+class Win32Test : public testing::Test {
+ public:
+  Win32Test() {
+  }
+};
+
+TEST_F(Win32Test, FileTimeToUInt64Test) {
+  FILETIME ft;
+  ft.dwHighDateTime = 0xBAADF00D;
+  ft.dwLowDateTime = 0xFEED3456;
+
+  uint64 expected = 0xBAADF00DFEED3456;
+  EXPECT_EQ(expected, ToUInt64(ft));
+}
+
+}  // namespace talk_base
diff --git a/talk/base/win32regkey.cc b/talk/base/win32regkey.cc
new file mode 100644
index 0000000..de74327
--- /dev/null
+++ b/talk/base/win32regkey.cc
@@ -0,0 +1,1119 @@
+/*
+ * libjingle
+ * Copyright 2003-2008, 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.
+ */
+
+// Registry configuration wrapers class implementation
+//
+// Change made by S. Ganesh - ganesh@google.com:
+//   Use SHQueryValueEx instead of RegQueryValueEx throughout.
+//   A call to the SHLWAPI function is essentially a call to the standard
+//   function but with post-processing:
+//   * to fix REG_SZ or REG_EXPAND_SZ data that is not properly null-terminated;
+//   * to expand REG_EXPAND_SZ data.
+
+#include "talk/base/win32regkey.h"
+
+#include <shlwapi.h>
+
+#include "talk/base/common.h"
+#include "talk/base/logging.h"
+#include "talk/base/scoped_ptr.h"
+
+namespace talk_base {
+
+RegKey::RegKey() {
+  h_key_ = NULL;
+}
+
+RegKey::~RegKey() {
+  Close();
+}
+
+HRESULT RegKey::Create(HKEY parent_key, const wchar_t* key_name) {
+  return Create(parent_key,
+                key_name,
+                REG_NONE,
+                REG_OPTION_NON_VOLATILE,
+                KEY_ALL_ACCESS,
+                NULL,
+                NULL);
+}
+
+HRESULT RegKey::Open(HKEY parent_key, const wchar_t* key_name) {
+  return Open(parent_key, key_name, KEY_ALL_ACCESS);
+}
+
+bool RegKey::HasValue(const TCHAR* value_name) const {
+  return (ERROR_SUCCESS == ::RegQueryValueEx(h_key_, value_name, NULL,
+                                             NULL, NULL, NULL));
+}
+
+HRESULT RegKey::SetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         DWORD value) {
+  ASSERT(full_key_name != NULL);
+
+  return SetValueStaticHelper(full_key_name, value_name, REG_DWORD, &value);
+}
+
+HRESULT RegKey::SetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         DWORD64 value) {
+  ASSERT(full_key_name != NULL);
+
+  return SetValueStaticHelper(full_key_name, value_name, REG_QWORD, &value);
+}
+
+HRESULT RegKey::SetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         float value) {
+  ASSERT(full_key_name != NULL);
+
+  return SetValueStaticHelper(full_key_name, value_name,
+                              REG_BINARY, &value, sizeof(value));
+}
+
+HRESULT RegKey::SetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         double value) {
+  ASSERT(full_key_name != NULL);
+
+  return SetValueStaticHelper(full_key_name, value_name,
+                              REG_BINARY, &value, sizeof(value));
+}
+
+HRESULT RegKey::SetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         const TCHAR* value) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(value != NULL);
+
+  return SetValueStaticHelper(full_key_name, value_name,
+                              REG_SZ, const_cast<wchar_t*>(value));
+}
+
+HRESULT RegKey::SetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         const uint8* value,
+                         DWORD byte_count) {
+  ASSERT(full_key_name != NULL);
+
+  return SetValueStaticHelper(full_key_name, value_name, REG_BINARY,
+                              const_cast<uint8*>(value), byte_count);
+}
+
+HRESULT RegKey::SetValueMultiSZ(const wchar_t* full_key_name,
+                                const wchar_t* value_name,
+                                const uint8* value,
+                                DWORD byte_count) {
+  ASSERT(full_key_name != NULL);
+
+  return SetValueStaticHelper(full_key_name, value_name, REG_MULTI_SZ,
+                              const_cast<uint8*>(value), byte_count);
+}
+
+HRESULT RegKey::GetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         DWORD* value) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(value != NULL);
+
+  return GetValueStaticHelper(full_key_name, value_name, REG_DWORD, value);
+}
+
+HRESULT RegKey::GetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         DWORD64* value) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(value != NULL);
+
+  return GetValueStaticHelper(full_key_name, value_name, REG_QWORD, value);
+}
+
+HRESULT RegKey::GetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         float* value) {
+  ASSERT(value != NULL);
+  ASSERT(full_key_name != NULL);
+
+  DWORD byte_count = 0;
+  scoped_array<byte> buffer;
+  HRESULT hr = GetValueStaticHelper(full_key_name, value_name,
+                                    REG_BINARY, buffer.accept(), &byte_count);
+  if (SUCCEEDED(hr)) {
+    ASSERT(byte_count == sizeof(*value));
+    if (byte_count == sizeof(*value)) {
+      *value = *reinterpret_cast<float*>(buffer.get());
+    }
+  }
+  return hr;
+}
+
+HRESULT RegKey::GetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         double* value) {
+  ASSERT(value != NULL);
+  ASSERT(full_key_name != NULL);
+
+  DWORD byte_count = 0;
+  scoped_array<byte> buffer;
+  HRESULT hr = GetValueStaticHelper(full_key_name, value_name,
+                                    REG_BINARY, buffer.accept(), &byte_count);
+  if (SUCCEEDED(hr)) {
+    ASSERT(byte_count == sizeof(*value));
+    if (byte_count == sizeof(*value)) {
+      *value = *reinterpret_cast<double*>(buffer.get());
+    }
+  }
+  return hr;
+}
+
+HRESULT RegKey::GetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         wchar_t** value) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(value != NULL);
+
+  return GetValueStaticHelper(full_key_name, value_name, REG_SZ, value);
+}
+
+HRESULT RegKey::GetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         std::wstring* value) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(value != NULL);
+
+  scoped_array<wchar_t> buffer;
+  HRESULT hr = RegKey::GetValue(full_key_name, value_name, buffer.accept());
+  if (SUCCEEDED(hr)) {
+    value->assign(buffer.get());
+  }
+  return hr;
+}
+
+HRESULT RegKey::GetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         std::vector<std::wstring>* value) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(value != NULL);
+
+  return GetValueStaticHelper(full_key_name, value_name, REG_MULTI_SZ, value);
+}
+
+HRESULT RegKey::GetValue(const wchar_t* full_key_name,
+                         const wchar_t* value_name,
+                         uint8** value,
+                         DWORD* byte_count) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(value != NULL);
+  ASSERT(byte_count != NULL);
+
+  return GetValueStaticHelper(full_key_name, value_name,
+                              REG_BINARY, value, byte_count);
+}
+
+HRESULT RegKey::DeleteSubKey(const wchar_t* key_name) {
+  ASSERT(key_name != NULL);
+  ASSERT(h_key_ != NULL);
+
+  LONG res = ::RegDeleteKey(h_key_, key_name);
+  HRESULT hr = HRESULT_FROM_WIN32(res);
+  if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) ||
+      hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)) {
+    hr = S_FALSE;
+  }
+  return hr;
+}
+
+HRESULT RegKey::DeleteValue(const wchar_t* value_name) {
+  ASSERT(h_key_ != NULL);
+
+  LONG res = ::RegDeleteValue(h_key_, value_name);
+  HRESULT hr = HRESULT_FROM_WIN32(res);
+  if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) ||
+      hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)) {
+    hr = S_FALSE;
+  }
+  return hr;
+}
+
+HRESULT RegKey::Close() {
+  HRESULT hr = S_OK;
+  if (h_key_ != NULL) {
+    LONG res = ::RegCloseKey(h_key_);
+    hr = HRESULT_FROM_WIN32(res);
+    h_key_ = NULL;
+  }
+  return hr;
+}
+
+HRESULT RegKey::Create(HKEY parent_key,
+                       const wchar_t* key_name,
+                       wchar_t* lpszClass,
+                       DWORD options,
+                       REGSAM sam_desired,
+                       LPSECURITY_ATTRIBUTES lpSecAttr,
+                       LPDWORD lpdwDisposition) {
+  ASSERT(key_name != NULL);
+  ASSERT(parent_key != NULL);
+
+  DWORD dw = 0;
+  HKEY h_key = NULL;
+  LONG res = ::RegCreateKeyEx(parent_key, key_name, 0, lpszClass, options,
+                              sam_desired, lpSecAttr, &h_key, &dw);
+  HRESULT hr = HRESULT_FROM_WIN32(res);
+
+  if (lpdwDisposition) {
+    *lpdwDisposition = dw;
+  }
+
+  // we have to close the currently opened key
+  // before replacing it with the new one
+  if (hr == S_OK) {
+    hr = Close();
+    ASSERT(hr == S_OK);
+    h_key_ = h_key;
+  }
+  return hr;
+}
+
+HRESULT RegKey::Open(HKEY parent_key,
+                     const wchar_t* key_name,
+                     REGSAM sam_desired) {
+  ASSERT(key_name != NULL);
+  ASSERT(parent_key != NULL);
+
+  HKEY h_key = NULL;
+  LONG res = ::RegOpenKeyEx(parent_key, key_name, 0, sam_desired, &h_key);
+  HRESULT hr = HRESULT_FROM_WIN32(res);
+
+  // we have to close the currently opened key
+  // before replacing it with the new one
+  if (hr == S_OK) {
+    // close the currently opened key if any
+    hr = Close();
+    ASSERT(hr == S_OK);
+    h_key_ = h_key;
+  }
+  return hr;
+}
+
+// save the key and all of its subkeys and values to a file
+HRESULT RegKey::Save(const wchar_t* full_key_name, const wchar_t* file_name) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(file_name != NULL);
+
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+  if (!h_key) {
+    return E_FAIL;
+  }
+
+  RegKey key;
+  HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_READ);
+  if (FAILED(hr)) {
+    return hr;
+  }
+
+  AdjustCurrentProcessPrivilege(SE_BACKUP_NAME, true);
+  LONG res = ::RegSaveKey(key.h_key_, file_name, NULL);
+  AdjustCurrentProcessPrivilege(SE_BACKUP_NAME, false);
+
+  return HRESULT_FROM_WIN32(res);
+}
+
+// restore the key and all of its subkeys and values which are saved into a file
+HRESULT RegKey::Restore(const wchar_t* full_key_name,
+                        const wchar_t* file_name) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(file_name != NULL);
+
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+  if (!h_key) {
+    return E_FAIL;
+  }
+
+  RegKey key;
+  HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_WRITE);
+  if (FAILED(hr)) {
+    return hr;
+  }
+
+  AdjustCurrentProcessPrivilege(SE_RESTORE_NAME, true);
+  LONG res = ::RegRestoreKey(key.h_key_, file_name, REG_FORCE_RESTORE);
+  AdjustCurrentProcessPrivilege(SE_RESTORE_NAME, false);
+
+  return HRESULT_FROM_WIN32(res);
+}
+
+// check if the current key has the specified subkey
+bool RegKey::HasSubkey(const wchar_t* key_name) const {
+  ASSERT(key_name != NULL);
+
+  RegKey key;
+  HRESULT hr = key.Open(h_key_, key_name, KEY_READ);
+  key.Close();
+  return hr == S_OK;
+}
+
+// static flush key
+HRESULT RegKey::FlushKey(const wchar_t* full_key_name) {
+  ASSERT(full_key_name != NULL);
+
+  HRESULT hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
+  // get the root HKEY
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+
+  if (h_key != NULL) {
+    LONG res = ::RegFlushKey(h_key);
+    hr = HRESULT_FROM_WIN32(res);
+  }
+  return hr;
+}
+
+// static SET helper
+HRESULT RegKey::SetValueStaticHelper(const wchar_t* full_key_name,
+                                     const wchar_t* value_name,
+                                     DWORD type,
+                                     LPVOID value,
+                                     DWORD byte_count) {
+  ASSERT(full_key_name != NULL);
+
+  HRESULT hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
+  // get the root HKEY
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+
+  if (h_key != NULL) {
+    RegKey key;
+    hr = key.Create(h_key, key_name.c_str());
+    if (hr == S_OK) {
+      switch (type) {
+        case REG_DWORD:
+          hr = key.SetValue(value_name, *(static_cast<DWORD*>(value)));
+          break;
+        case REG_QWORD:
+          hr = key.SetValue(value_name, *(static_cast<DWORD64*>(value)));
+          break;
+        case REG_SZ:
+          hr = key.SetValue(value_name, static_cast<const wchar_t*>(value));
+          break;
+        case REG_BINARY:
+          hr = key.SetValue(value_name, static_cast<const uint8*>(value),
+                            byte_count);
+          break;
+        case REG_MULTI_SZ:
+          hr = key.SetValue(value_name, static_cast<const uint8*>(value),
+                            byte_count, type);
+          break;
+        default:
+          ASSERT(false);
+          hr = HRESULT_FROM_WIN32(ERROR_DATATYPE_MISMATCH);
+          break;
+      }
+      // close the key after writing
+      HRESULT temp_hr = key.Close();
+      if (hr == S_OK) {
+        hr = temp_hr;
+      }
+    }
+  }
+  return hr;
+}
+
+// static GET helper
+HRESULT RegKey::GetValueStaticHelper(const wchar_t* full_key_name,
+                                     const wchar_t* value_name,
+                                     DWORD type,
+                                     LPVOID value,
+                                     DWORD* byte_count) {
+  ASSERT(full_key_name != NULL);
+
+  HRESULT hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
+  // get the root HKEY
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+
+  if (h_key != NULL) {
+    RegKey key;
+    hr = key.Open(h_key, key_name.c_str(), KEY_READ);
+    if (hr == S_OK) {
+      switch (type) {
+        case REG_DWORD:
+          hr = key.GetValue(value_name, reinterpret_cast<DWORD*>(value));
+          break;
+        case REG_QWORD:
+          hr = key.GetValue(value_name, reinterpret_cast<DWORD64*>(value));
+          break;
+        case REG_SZ:
+          hr = key.GetValue(value_name, reinterpret_cast<wchar_t**>(value));
+          break;
+        case REG_MULTI_SZ:
+          hr = key.GetValue(value_name, reinterpret_cast<
+                                            std::vector<std::wstring>*>(value));
+          break;
+        case REG_BINARY:
+          hr = key.GetValue(value_name, reinterpret_cast<uint8**>(value),
+                            byte_count);
+          break;
+        default:
+          ASSERT(false);
+          hr = HRESULT_FROM_WIN32(ERROR_DATATYPE_MISMATCH);
+          break;
+      }
+      // close the key after writing
+      HRESULT temp_hr = key.Close();
+      if (hr == S_OK) {
+        hr = temp_hr;
+      }
+    }
+  }
+  return hr;
+}
+
+// GET helper
+HRESULT RegKey::GetValueHelper(const wchar_t* value_name,
+                               DWORD* type,
+                               uint8** value,
+                               DWORD* byte_count) const {
+  ASSERT(byte_count != NULL);
+  ASSERT(value != NULL);
+  ASSERT(type != NULL);
+
+  // init return buffer
+  *value = NULL;
+
+  // get the size of the return data buffer
+  LONG res = ::SHQueryValueEx(h_key_, value_name, NULL, type, NULL, byte_count);
+  HRESULT hr = HRESULT_FROM_WIN32(res);
+
+  if (hr == S_OK) {
+    // if the value length is 0, nothing to do
+    if (*byte_count != 0) {
+      // allocate the buffer
+      *value = new byte[*byte_count];
+      ASSERT(*value != NULL);
+
+      // make the call again to get the data
+      res = ::SHQueryValueEx(h_key_, value_name, NULL,
+                             type, *value, byte_count);
+      hr = HRESULT_FROM_WIN32(res);
+      ASSERT(hr == S_OK);
+    }
+  }
+  return hr;
+}
+
+// Int32 Get
+HRESULT RegKey::GetValue(const wchar_t* value_name, DWORD* value) const {
+  ASSERT(value != NULL);
+
+  DWORD type = 0;
+  DWORD byte_count = sizeof(DWORD);
+  LONG res = ::SHQueryValueEx(h_key_, value_name, NULL, &type,
+                              value, &byte_count);
+  HRESULT hr = HRESULT_FROM_WIN32(res);
+  ASSERT((hr != S_OK) || (type == REG_DWORD));
+  ASSERT((hr != S_OK) || (byte_count == sizeof(DWORD)));
+  return hr;
+}
+
+// Int64 Get
+HRESULT RegKey::GetValue(const wchar_t* value_name, DWORD64* value) const {
+  ASSERT(value != NULL);
+
+  DWORD type = 0;
+  DWORD byte_count = sizeof(DWORD64);
+  LONG res = ::SHQueryValueEx(h_key_, value_name, NULL, &type,
+                              value, &byte_count);
+  HRESULT hr = HRESULT_FROM_WIN32(res);
+  ASSERT((hr != S_OK) || (type == REG_QWORD));
+  ASSERT((hr != S_OK) || (byte_count == sizeof(DWORD64)));
+  return hr;
+}
+
+// String Get
+HRESULT RegKey::GetValue(const wchar_t* value_name, wchar_t** value) const {
+  ASSERT(value != NULL);
+
+  DWORD byte_count = 0;
+  DWORD type = 0;
+
+  // first get the size of the string buffer
+  LONG res = ::SHQueryValueEx(h_key_, value_name, NULL,
+                              &type, NULL, &byte_count);
+  HRESULT hr = HRESULT_FROM_WIN32(res);
+
+  if (hr == S_OK) {
+    // allocate room for the string and a terminating \0
+    *value = new wchar_t[(byte_count / sizeof(wchar_t)) + 1];
+
+    if ((*value) != NULL) {
+      if (byte_count != 0) {
+        // make the call again
+        res = ::SHQueryValueEx(h_key_, value_name, NULL, &type,
+                               *value, &byte_count);
+        hr = HRESULT_FROM_WIN32(res);
+      } else {
+        (*value)[0] = L'\0';
+      }
+
+      ASSERT((hr != S_OK) || (type == REG_SZ) ||
+             (type == REG_MULTI_SZ) || (type == REG_EXPAND_SZ));
+    } else {
+      hr = E_OUTOFMEMORY;
+    }
+  }
+
+  return hr;
+}
+
+// get a string value
+HRESULT RegKey::GetValue(const wchar_t* value_name, std::wstring* value) const {
+  ASSERT(value != NULL);
+
+  DWORD byte_count = 0;
+  DWORD type = 0;
+
+  // first get the size of the string buffer
+  LONG res = ::SHQueryValueEx(h_key_, value_name, NULL,
+                              &type, NULL, &byte_count);
+  HRESULT hr = HRESULT_FROM_WIN32(res);
+
+  if (hr == S_OK) {
+    if (byte_count != 0) {
+      // Allocate some memory and make the call again
+      value->resize(byte_count / sizeof(wchar_t) + 1);
+      res = ::SHQueryValueEx(h_key_, value_name, NULL, &type,
+                             &value->at(0), &byte_count);
+      hr = HRESULT_FROM_WIN32(res);
+      value->resize(wcslen(value->data()));
+    } else {
+      value->clear();
+    }
+
+    ASSERT((hr != S_OK) || (type == REG_SZ) ||
+           (type == REG_MULTI_SZ) || (type == REG_EXPAND_SZ));
+  }
+
+  return hr;
+}
+
+// convert REG_MULTI_SZ bytes to string array
+HRESULT RegKey::MultiSZBytesToStringArray(const uint8* buffer,
+                                          DWORD byte_count,
+                                          std::vector<std::wstring>* value) {
+  ASSERT(buffer != NULL);
+  ASSERT(value != NULL);
+
+  const wchar_t* data = reinterpret_cast<const wchar_t*>(buffer);
+  DWORD data_len = byte_count / sizeof(wchar_t);
+  value->clear();
+  if (data_len > 1) {
+    // must be terminated by two null characters
+    if (data[data_len - 1] != 0 || data[data_len - 2] != 0) {
+      return E_INVALIDARG;
+    }
+
+    // put null-terminated strings into arrays
+    while (*data) {
+      std::wstring str(data);
+      value->push_back(str);
+      data += str.length() + 1;
+    }
+  }
+  return S_OK;
+}
+
+// get a std::vector<std::wstring> value from REG_MULTI_SZ type
+HRESULT RegKey::GetValue(const wchar_t* value_name,
+                         std::vector<std::wstring>* value) const {
+  ASSERT(value != NULL);
+
+  DWORD byte_count = 0;
+  DWORD type = 0;
+  uint8* buffer = 0;
+
+  // first get the size of the buffer
+  HRESULT hr = GetValueHelper(value_name, &type, &buffer, &byte_count);
+  ASSERT((hr != S_OK) || (type == REG_MULTI_SZ));
+
+  if (SUCCEEDED(hr)) {
+    hr = MultiSZBytesToStringArray(buffer, byte_count, value);
+  }
+
+  return hr;
+}
+
+// Binary data Get
+HRESULT RegKey::GetValue(const wchar_t* value_name,
+                         uint8** value,
+                         DWORD* byte_count) const {
+  ASSERT(byte_count != NULL);
+  ASSERT(value != NULL);
+
+  DWORD type = 0;
+  HRESULT hr = GetValueHelper(value_name, &type, value, byte_count);
+  ASSERT((hr != S_OK) || (type == REG_MULTI_SZ) || (type == REG_BINARY));
+  return hr;
+}
+
+// Raw data get
+HRESULT RegKey::GetValue(const wchar_t* value_name,
+                         uint8** value,
+                         DWORD* byte_count,
+                         DWORD*type) const {
+  ASSERT(type != NULL);
+  ASSERT(byte_count != NULL);
+  ASSERT(value != NULL);
+
+  return GetValueHelper(value_name, type, value, byte_count);
+}
+
+// Int32 set
+HRESULT RegKey::SetValue(const wchar_t* value_name, DWORD value) const {
+  ASSERT(h_key_ != NULL);
+
+  LONG res = ::RegSetValueEx(h_key_, value_name, NULL, REG_DWORD,
+                             reinterpret_cast<const uint8*>(&value),
+                             sizeof(DWORD));
+  return HRESULT_FROM_WIN32(res);
+}
+
+// Int64 set
+HRESULT RegKey::SetValue(const wchar_t* value_name, DWORD64 value) const {
+  ASSERT(h_key_ != NULL);
+
+  LONG res = ::RegSetValueEx(h_key_, value_name, NULL, REG_QWORD,
+                             reinterpret_cast<const uint8*>(&value),
+                             sizeof(DWORD64));
+  return HRESULT_FROM_WIN32(res);
+}
+
+// String set
+HRESULT RegKey::SetValue(const wchar_t* value_name,
+                         const wchar_t* value) const {
+  ASSERT(value != NULL);
+  ASSERT(h_key_ != NULL);
+
+  LONG res = ::RegSetValueEx(h_key_, value_name, NULL, REG_SZ,
+                             reinterpret_cast<const uint8*>(value),
+                             (lstrlen(value) + 1) * sizeof(wchar_t));
+  return HRESULT_FROM_WIN32(res);
+}
+
+// Binary data set
+HRESULT RegKey::SetValue(const wchar_t* value_name,
+                         const uint8* value,
+                         DWORD byte_count) const {
+  ASSERT(h_key_ != NULL);
+
+  // special case - if 'value' is NULL make sure byte_count is zero
+  if (value == NULL) {
+    byte_count = 0;
+  }
+
+  LONG res = ::RegSetValueEx(h_key_, value_name, NULL,
+                             REG_BINARY, value, byte_count);
+  return HRESULT_FROM_WIN32(res);
+}
+
+// Raw data set
+HRESULT RegKey::SetValue(const wchar_t* value_name,
+                         const uint8* value,
+                         DWORD byte_count,
+                         DWORD type) const {
+  ASSERT(value != NULL);
+  ASSERT(h_key_ != NULL);
+
+  LONG res = ::RegSetValueEx(h_key_, value_name, NULL, type, value, byte_count);
+  return HRESULT_FROM_WIN32(res);
+}
+
+bool RegKey::HasKey(const wchar_t* full_key_name) {
+  ASSERT(full_key_name != NULL);
+
+  // get the root HKEY
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+
+  if (h_key != NULL) {
+    RegKey key;
+    HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_READ);
+    key.Close();
+    return S_OK == hr;
+  }
+  return false;
+}
+
+// static version of HasValue
+bool RegKey::HasValue(const wchar_t* full_key_name, const wchar_t* value_name) {
+  ASSERT(full_key_name != NULL);
+
+  bool has_value = false;
+  // get the root HKEY
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+
+  if (h_key != NULL) {
+    RegKey key;
+    if (key.Open(h_key, key_name.c_str(), KEY_READ) == S_OK) {
+      has_value = key.HasValue(value_name);
+      key.Close();
+    }
+  }
+  return has_value;
+}
+
+HRESULT RegKey::GetValueType(const wchar_t* full_key_name,
+                             const wchar_t* value_name,
+                             DWORD* value_type) {
+  ASSERT(full_key_name != NULL);
+  ASSERT(value_type != NULL);
+
+  *value_type = REG_NONE;
+
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+
+  RegKey key;
+  HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_READ);
+  if (SUCCEEDED(hr)) {
+    LONG res = ::SHQueryValueEx(key.h_key_, value_name, NULL, value_type,
+                                NULL, NULL);
+    if (res != ERROR_SUCCESS) {
+      hr = HRESULT_FROM_WIN32(res);
+    }
+  }
+
+  return hr;
+}
+
+HRESULT RegKey::DeleteKey(const wchar_t* full_key_name) {
+  ASSERT(full_key_name != NULL);
+
+  return DeleteKey(full_key_name, true);
+}
+
+HRESULT RegKey::DeleteKey(const wchar_t* full_key_name, bool recursively) {
+  ASSERT(full_key_name != NULL);
+
+  // need to open the parent key first
+  // get the root HKEY
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+
+  // get the parent key
+  std::wstring parent_key(GetParentKeyInfo(&key_name));
+
+  RegKey key;
+  HRESULT hr = key.Open(h_key, parent_key.c_str());
+
+  if (hr == S_OK) {
+    hr = recursively ? key.RecurseDeleteSubKey(key_name.c_str())
+                     : key.DeleteSubKey(key_name.c_str());
+  } else if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) ||
+             hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)) {
+    hr = S_FALSE;
+  }
+
+  key.Close();
+  return hr;
+}
+
+HRESULT RegKey::DeleteValue(const wchar_t* full_key_name,
+                            const wchar_t* value_name) {
+  ASSERT(full_key_name != NULL);
+
+  HRESULT hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
+  // get the root HKEY
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+
+  if (h_key != NULL) {
+    RegKey key;
+    hr = key.Open(h_key, key_name.c_str());
+    if (hr == S_OK) {
+      hr = key.DeleteValue(value_name);
+      key.Close();
+    }
+  }
+  return hr;
+}
+
+HRESULT RegKey::RecurseDeleteSubKey(const wchar_t* key_name) {
+  ASSERT(key_name != NULL);
+
+  RegKey key;
+  HRESULT hr = key.Open(h_key_, key_name);
+
+  if (hr == S_OK) {
+    // enumerate all subkeys of this key and recursivelly delete them
+    FILETIME time = {0};
+    wchar_t key_name_buf[kMaxKeyNameChars] = {0};
+    DWORD key_name_buf_size = kMaxKeyNameChars;
+    while (hr == S_OK &&
+        ::RegEnumKeyEx(key.h_key_, 0, key_name_buf, &key_name_buf_size,
+                       NULL, NULL, NULL,  &time) == ERROR_SUCCESS) {
+      hr = key.RecurseDeleteSubKey(key_name_buf);
+
+      // restore the buffer size
+      key_name_buf_size = kMaxKeyNameChars;
+    }
+    // close the top key
+    key.Close();
+  }
+
+  if (hr == S_OK) {
+    // the key has no more children keys
+    // delete the key and all of its values
+    hr = DeleteSubKey(key_name);
+  }
+
+  return hr;
+}
+
+HKEY RegKey::GetRootKeyInfo(std::wstring* full_key_name) {
+  ASSERT(full_key_name != NULL);
+
+  HKEY h_key = NULL;
+  // get the root HKEY
+  int index = full_key_name->find(L'\\');
+  std::wstring root_key;
+
+  if (index == -1) {
+    root_key = *full_key_name;
+    *full_key_name = L"";
+  } else {
+    root_key = full_key_name->substr(0, index);
+    *full_key_name = full_key_name->substr(index + 1,
+                                           full_key_name->length() - index - 1);
+  }
+
+  for (std::wstring::iterator iter = root_key.begin();
+       iter != root_key.end(); ++iter) {
+    *iter = toupper(*iter);
+  }
+
+  if (!root_key.compare(L"HKLM") ||
+      !root_key.compare(L"HKEY_LOCAL_MACHINE")) {
+    h_key = HKEY_LOCAL_MACHINE;
+  } else if (!root_key.compare(L"HKCU") ||
+             !root_key.compare(L"HKEY_CURRENT_USER")) {
+    h_key = HKEY_CURRENT_USER;
+  } else if (!root_key.compare(L"HKU") ||
+             !root_key.compare(L"HKEY_USERS")) {
+    h_key = HKEY_USERS;
+  } else if (!root_key.compare(L"HKCR") ||
+             !root_key.compare(L"HKEY_CLASSES_ROOT")) {
+    h_key = HKEY_CLASSES_ROOT;
+  }
+
+  return h_key;
+}
+
+
+// Returns true if this key name is 'safe' for deletion
+// (doesn't specify a key root)
+bool RegKey::SafeKeyNameForDeletion(const wchar_t* key_name) {
+  ASSERT(key_name != NULL);
+  std::wstring key(key_name);
+
+  HKEY root_key = GetRootKeyInfo(&key);
+
+  if (!root_key) {
+    key = key_name;
+  }
+  if (key.empty()) {
+    return false;
+  }
+  bool found_subkey = false, backslash_found = false;
+  for (size_t i = 0 ; i < key.length() ; ++i) {
+    if (key[i] == L'\\') {
+      backslash_found = true;
+    } else if (backslash_found) {
+      found_subkey = true;
+      break;
+    }
+  }
+  return (root_key == HKEY_USERS) ? found_subkey : true;
+}
+
+std::wstring RegKey::GetParentKeyInfo(std::wstring* key_name) {
+  ASSERT(key_name != NULL);
+
+  // get the parent key
+  int index = key_name->rfind(L'\\');
+  std::wstring parent_key;
+  if (index == -1) {
+    parent_key = L"";
+  } else {
+    parent_key = key_name->substr(0, index);
+    *key_name = key_name->substr(index + 1, key_name->length() - index - 1);
+  }
+
+  return parent_key;
+}
+
+// get the number of values for this key
+uint32 RegKey::GetValueCount() {
+  DWORD num_values = 0;
+
+  LONG res = ::RegQueryInfoKey(
+        h_key_,                  // key handle
+        NULL,                    // buffer for class name
+        NULL,                    // size of class string
+        NULL,                    // reserved
+        NULL,                    // number of subkeys
+        NULL,                    // longest subkey size
+        NULL,                    // longest class string
+        &num_values,             // number of values for this key
+        NULL,                    // longest value name
+        NULL,                    // longest value data
+        NULL,                    // security descriptor
+        NULL);                   // last write time
+
+  ASSERT(res == ERROR_SUCCESS);
+  return num_values;
+}
+
+// Enumerators for the value_names for this key
+
+// Called to get the value name for the given value name index
+// Use GetValueCount() to get the total value_name count for this key
+// Returns failure if no key at the specified index
+HRESULT RegKey::GetValueNameAt(int index, std::wstring* value_name,
+                               DWORD* type) {
+  ASSERT(value_name != NULL);
+
+  LONG res = ERROR_SUCCESS;
+  wchar_t value_name_buf[kMaxValueNameChars] = {0};
+  DWORD value_name_buf_size = kMaxValueNameChars;
+  res = ::RegEnumValue(h_key_, index, value_name_buf, &value_name_buf_size,
+                       NULL, type, NULL, NULL);
+
+  if (res == ERROR_SUCCESS) {
+    value_name->assign(value_name_buf);
+  }
+
+  return HRESULT_FROM_WIN32(res);
+}
+
+uint32 RegKey::GetSubkeyCount() {
+  // number of values for key
+  DWORD num_subkeys = 0;
+
+  LONG res = ::RegQueryInfoKey(
+    h_key_,                  // key handle
+    NULL,                    // buffer for class name
+    NULL,                    // size of class string
+    NULL,                    // reserved
+    &num_subkeys,            // number of subkeys
+    NULL,                    // longest subkey size
+    NULL,                    // longest class string
+    NULL,                    // number of values for this key
+    NULL,                    // longest value name
+    NULL,                    // longest value data
+    NULL,                    // security descriptor
+    NULL);                   // last write time
+
+  ASSERT(res == ERROR_SUCCESS);
+  return num_subkeys;
+}
+
+HRESULT RegKey::GetSubkeyNameAt(int index, std::wstring* key_name) {
+  ASSERT(key_name != NULL);
+
+  LONG res = ERROR_SUCCESS;
+  wchar_t key_name_buf[kMaxKeyNameChars] = {0};
+  DWORD key_name_buf_size = kMaxKeyNameChars;
+
+  res = ::RegEnumKeyEx(h_key_, index, key_name_buf, &key_name_buf_size,
+                       NULL, NULL, NULL, NULL);
+
+  if (res == ERROR_SUCCESS) {
+    key_name->assign(key_name_buf);
+  }
+
+  return HRESULT_FROM_WIN32(res);
+}
+
+// Is the key empty: having no sub-keys and values
+bool RegKey::IsKeyEmpty(const wchar_t* full_key_name) {
+  ASSERT(full_key_name != NULL);
+
+  bool is_empty = true;
+
+  // Get the root HKEY
+  std::wstring key_name(full_key_name);
+  HKEY h_key = GetRootKeyInfo(&key_name);
+
+  // Open the key to check
+  if (h_key != NULL) {
+    RegKey key;
+    HRESULT hr = key.Open(h_key, key_name.c_str(), KEY_READ);
+    if (SUCCEEDED(hr)) {
+      is_empty = key.GetSubkeyCount() == 0 && key.GetValueCount() == 0;
+      key.Close();
+    }
+  }
+
+  return is_empty;
+}
+
+bool AdjustCurrentProcessPrivilege(const TCHAR* privilege, bool to_enable) {
+  ASSERT(privilege != NULL);
+
+  bool ret = false;
+  HANDLE token;
+  if (::OpenProcessToken(::GetCurrentProcess(),
+                         TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) {
+    LUID luid;
+    memset(&luid, 0, sizeof(luid));
+    if (::LookupPrivilegeValue(NULL, privilege, &luid)) {
+      TOKEN_PRIVILEGES privs;
+      privs.PrivilegeCount = 1;
+      privs.Privileges[0].Luid = luid;
+      privs.Privileges[0].Attributes = to_enable ? SE_PRIVILEGE_ENABLED : 0;
+      if (::AdjustTokenPrivileges(token, FALSE, &privs, 0, NULL, 0)) {
+        ret = true;
+      } else {
+        LOG_GLE(LS_ERROR) << "AdjustTokenPrivileges failed";
+      }
+    } else {
+      LOG_GLE(LS_ERROR) << "LookupPrivilegeValue failed";
+    }
+    CloseHandle(token);
+  } else {
+    LOG_GLE(LS_ERROR) << "OpenProcessToken(GetCurrentProcess) failed";
+  }
+
+  return ret;
+}
+
+}  // namespace talk_base
diff --git a/talk/base/win32regkey.h b/talk/base/win32regkey.h
new file mode 100644
index 0000000..9f01ce1
--- /dev/null
+++ b/talk/base/win32regkey.h
@@ -0,0 +1,354 @@
+/*
+ * libjingle
+ * Copyright 2003-2007, 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.
+ */
+
+// Registry configuration wrappers class
+//
+// Offers static functions for convenient
+// fast access for individual values
+//
+// Also provides a wrapper class for efficient
+// batch operations on values of a given registry key.
+//
+
+#ifndef TALK_BASE_WIN32REGKEY_H_
+#define TALK_BASE_WIN32REGKEY_H_
+
+#include <string>
+#include <vector>
+
+#include "talk/base/basictypes.h"
+#include "talk/base/win32.h"
+
+namespace talk_base {
+
+// maximum sizes registry key and value names
+const int kMaxKeyNameChars = 255 + 1;
+const int kMaxValueNameChars = 16383 + 1;
+
+class RegKey {
+ public:
+  // constructor
+  RegKey();
+
+  // destructor
+  ~RegKey();
+
+  // create a reg key
+  HRESULT Create(HKEY parent_key, const wchar_t* key_name);
+
+  HRESULT Create(HKEY parent_key,
+                 const wchar_t* key_name,
+                 wchar_t* reg_class,
+                 DWORD options,
+                 REGSAM sam_desired,
+                 LPSECURITY_ATTRIBUTES lp_sec_attr,
+                 LPDWORD lp_disposition);
+
+  // open an existing reg key
+  HRESULT Open(HKEY parent_key, const wchar_t* key_name);
+
+  HRESULT Open(HKEY parent_key, const wchar_t* key_name, REGSAM sam_desired);
+
+  // close this reg key
+  HRESULT Close();
+
+  // check if the key has a specified value
+  bool HasValue(const wchar_t* value_name) const;
+
+  // get the number of values for this key
+  uint32 GetValueCount();
+
+  // Called to get the value name for the given value name index
+  // Use GetValueCount() to get the total value_name count for this key
+  // Returns failure if no key at the specified index
+  // If you modify the key while enumerating, the indexes will be out of order.
+  // Since the index order is not guaranteed, you need to reset your counting
+  // loop.
+  // 'type' refers to REG_DWORD, REG_QWORD, etc..
+  // 'type' can be NULL if not interested in the value type
+  HRESULT GetValueNameAt(int index, std::wstring* value_name, DWORD* type);
+
+  // check if the current key has the specified subkey
+  bool HasSubkey(const wchar_t* key_name) const;
+
+  // get the number of subkeys for this key
+  uint32 GetSubkeyCount();
+
+  // Called to get the key name for the given key index
+  // Use GetSubkeyCount() to get the total count for this key
+  // Returns failure if no key at the specified index
+  // If you modify the key while enumerating, the indexes will be out of order.
+  // Since the index order is not guaranteed, you need to reset your counting
+  // loop.
+  HRESULT GetSubkeyNameAt(int index, std::wstring* key_name);
+
+  // SETTERS
+
+  // set an int32 value - use when reading multiple values from a key
+  HRESULT SetValue(const wchar_t* value_name, DWORD value) const;
+
+  // set an int64 value
+  HRESULT SetValue(const wchar_t* value_name, DWORD64 value) const;
+
+  // set a string value
+  HRESULT SetValue(const wchar_t* value_name, const wchar_t* value) const;
+
+  // set binary data
+  HRESULT SetValue(const wchar_t* value_name,
+                   const uint8* value,
+                   DWORD byte_count) const;
+
+  // set raw data, including type
+  HRESULT SetValue(const wchar_t* value_name,
+                   const uint8* value,
+                   DWORD byte_count,
+                   DWORD type) const;
+
+  // GETTERS
+
+  // get an int32 value
+  HRESULT GetValue(const wchar_t* value_name, DWORD* value) const;
+
+  // get an int64 value
+  HRESULT GetValue(const wchar_t* value_name, DWORD64* value) const;
+
+  // get a string value - the caller must free the return buffer
+  HRESULT GetValue(const wchar_t* value_name, wchar_t** value) const;
+
+  // get a string value
+  HRESULT GetValue(const wchar_t* value_name, std::wstring* value) const;
+
+  // get a std::vector<std::wstring> value from REG_MULTI_SZ type
+  HRESULT GetValue(const wchar_t* value_name,
+                   std::vector<std::wstring>* value) const;
+
+  // get binary data - the caller must free the return buffer
+  HRESULT GetValue(const wchar_t* value_name,
+                   uint8** value,
+                   DWORD* byte_count) const;
+
+  // get raw data, including type - the caller must free the return buffer
+  HRESULT GetValue(const wchar_t* value_name,
+                   uint8** value,
+                   DWORD* byte_count,
+                   DWORD* type) const;
+
+  // STATIC VERSIONS
+
+  // flush
+  static HRESULT FlushKey(const wchar_t* full_key_name);
+
+  // check if a key exists
+  static bool HasKey(const wchar_t* full_key_name);
+
+  // check if the key has a specified value
+  static bool HasValue(const wchar_t* full_key_name, const wchar_t* value_name);
+
+  // SETTERS
+
+  // STATIC int32 set
+  static HRESULT SetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          DWORD value);
+
+  // STATIC int64 set
+  static HRESULT SetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          DWORD64 value);
+
+  // STATIC float set
+  static HRESULT SetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          float value);
+
+  // STATIC double set
+  static HRESULT SetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          double value);
+
+  // STATIC string set
+  static HRESULT SetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          const wchar_t* value);
+
+  // STATIC binary data set
+  static HRESULT SetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          const uint8* value,
+                          DWORD byte_count);
+
+  // STATIC multi-string set
+  static HRESULT SetValueMultiSZ(const wchar_t* full_key_name,
+                                 const TCHAR* value_name,
+                                 const uint8* value,
+                                 DWORD byte_count);
+
+  // GETTERS
+
+  // STATIC int32 get
+  static HRESULT GetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          DWORD* value);
+
+  // STATIC int64 get
+  //
+  // Note: if you are using time64 you should
+  // likely use GetLimitedTimeValue (util.h) instead of this method.
+  static HRESULT GetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          DWORD64* value);
+
+  // STATIC float get
+  static HRESULT GetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          float* value);
+
+  // STATIC double get
+  static HRESULT GetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          double* value);
+
+  // STATIC string get
+  // Note: the caller must free the return buffer for wchar_t* version
+  static HRESULT GetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          wchar_t** value);
+  static HRESULT GetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          std::wstring* value);
+
+  // STATIC REG_MULTI_SZ get
+  static HRESULT GetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          std::vector<std::wstring>* value);
+
+  // STATIC get binary data - the caller must free the return buffer
+  static HRESULT GetValue(const wchar_t* full_key_name,
+                          const wchar_t* value_name,
+                          uint8** value,
+                          DWORD* byte_count);
+
+  // Get type of a registry value
+  static HRESULT GetValueType(const wchar_t* full_key_name,
+                              const wchar_t* value_name,
+                              DWORD* value_type);
+
+  // delete a subkey of the current key (with no subkeys)
+  HRESULT DeleteSubKey(const wchar_t* key_name);
+
+  // recursively delete a sub key of the current key (and all its subkeys)
+  HRESULT RecurseDeleteSubKey(const wchar_t* key_name);
+
+  // STATIC version of delete key - handles nested keys also
+  // delete a key and all its sub-keys recursively
+  // Returns S_FALSE if key didn't exist, S_OK if deletion was successful,
+  // and failure otherwise.
+  static HRESULT DeleteKey(const wchar_t* full_key_name);
+
+  // STATIC version of delete key
+  // delete a key recursively or non-recursively
+  // Returns S_FALSE if key didn't exist, S_OK if deletion was successful,
+  // and failure otherwise.
+  static HRESULT DeleteKey(const wchar_t* full_key_name, bool recursive);
+
+  // delete the specified value
+  HRESULT DeleteValue(const wchar_t* value_name);
+
+  // STATIC version of delete value
+  // Returns S_FALSE if key didn't exist, S_OK if deletion was successful,
+  // and failure otherwise.
+  static HRESULT DeleteValue(const wchar_t* full_key_name,
+                             const wchar_t* value_name);
+
+  // Peek inside (use a RegKey as a smart wrapper around a registry handle)
+  HKEY key() { return h_key_; }
+
+  // helper function to get the HKEY and the root key from a string
+  // modifies the argument in place and returns the key name
+  // e.g. HKLM\\Software\\Google\... returns HKLM, "Software\\Google\..."
+  // Necessary for the static versions that use the full name of the reg key
+  static HKEY GetRootKeyInfo(std::wstring* full_key_name);
+
+  // Returns true if this key name is 'safe' for deletion (doesn't specify a key
+  // root)
+  static bool SafeKeyNameForDeletion(const wchar_t* key_name);
+
+  // save the key and all of its subkeys and values to a file
+  static HRESULT Save(const wchar_t* full_key_name, const wchar_t* file_name);
+
+  // restore the key and all of its subkeys and values which are saved into a
+  // file
+  static HRESULT Restore(const wchar_t* full_key_name,
+                         const wchar_t* file_name);
+
+  // Is the key empty: having no sub-keys and values
+  static bool IsKeyEmpty(const wchar_t* full_key_name);
+
+ private:
+
+  // helper function to get any value from the registry
+  // used when the size of the data is unknown
+  HRESULT GetValueHelper(const wchar_t* value_name,
+                         DWORD* type, uint8** value,
+                         DWORD* byte_count) const;
+
+  // helper function to get the parent key name and the subkey from a string
+  // modifies the argument in place and returns the key name
+  // Necessary for the static versions that use the full name of the reg key
+  static std::wstring GetParentKeyInfo(std::wstring* key_name);
+
+  // common SET Helper for the static case
+  static HRESULT SetValueStaticHelper(const wchar_t* full_key_name,
+                                      const wchar_t* value_name,
+                                      DWORD type,
+                                      LPVOID value,
+                                      DWORD byte_count = 0);
+
+  // common GET Helper for the static case
+  static HRESULT GetValueStaticHelper(const wchar_t* full_key_name,
+                                      const wchar_t* value_name,
+                                      DWORD type,
+                                      LPVOID value,
+                                      DWORD* byte_count = NULL);
+
+  // convert REG_MULTI_SZ bytes to string array
+  static HRESULT MultiSZBytesToStringArray(const uint8* buffer,
+                                           DWORD byte_count,
+                                           std::vector<std::wstring>* value);
+
+  // the HKEY for the current key
+  HKEY h_key_;
+
+  // for unittest
+  friend void RegKeyHelperFunctionsTest();
+
+  DISALLOW_EVIL_CONSTRUCTORS(RegKey);
+};
+
+}  // namespace talk_base
+
+#endif  // TALK_BASE_WIN32REGKEY_H_
diff --git a/talk/base/win32regkey_unittest.cc b/talk/base/win32regkey_unittest.cc
new file mode 100644
index 0000000..1dd8fe4
--- /dev/null
+++ b/talk/base/win32regkey_unittest.cc
@@ -0,0 +1,607 @@
+/*
+ * libjingle
+ * Copyright 2003-2008, 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.
+ */
+
+// Unittest for registry access API
+
+#include "talk/base/gunit.h"
+#include "talk/base/common.h"
+#include "talk/base/win32regkey.h"
+
+namespace talk_base {
+
+#ifndef EXPECT_SUCCEEDED
+#define EXPECT_SUCCEEDED(x)  EXPECT_TRUE(SUCCEEDED(x))
+#endif
+
+#ifndef EXPECT_FAILED
+#define EXPECT_FAILED(x)  EXPECT_TRUE(FAILED(x))
+#endif
+
+#define kBaseKey           L"Software\\Google\\__TEST"
+#define kSubkeyName        L"subkey_test"
+
+const wchar_t kRkey1[] = kBaseKey;
+const wchar_t kRkey1SubkeyName[] = kSubkeyName;
+const wchar_t kRkey1Subkey[] = kBaseKey L"\\" kSubkeyName;
+const wchar_t kFullRkey1[] = L"HKCU\\" kBaseKey;
+const wchar_t kFullRkey1Subkey[] = L"HKCU\\" kBaseKey L"\\" kSubkeyName;
+
+const wchar_t kValNameInt[] = L"Int32 Value";
+const DWORD kIntVal = 20;
+const DWORD kIntVal2 = 30;
+
+const wchar_t kValNameInt64[] = L"Int64 Value";
+const DWORD64 kIntVal64 = 119600064000000000uI64;
+
+const wchar_t kValNameFloat[] = L"Float Value";
+const float kFloatVal = 12.3456789f;
+
+const wchar_t kValNameDouble[] = L"Double Value";
+const double kDoubleVal = 98.7654321;
+
+const wchar_t kValNameStr[] = L"Str Value";
+const wchar_t kStrVal[] = L"Some string data 1";
+const wchar_t kStrVal2[] = L"Some string data 2";
+
+const wchar_t kValNameBinary[] = L"Binary Value";
+const char kBinaryVal[] = "Some binary data abcdefghi 1";
+const char kBinaryVal2[] = "Some binary data abcdefghi 2";
+
+const wchar_t kValNameMultiStr[] = L"MultiStr Value";
+const wchar_t kMultiSZ[] = L"abc\0def\0P12345\0";
+const wchar_t kEmptyMultiSZ[] = L"";
+const wchar_t kInvalidMultiSZ[] = {L'6', L'7', L'8'};
+
+// friend function of RegKey
+void RegKeyHelperFunctionsTest() {
+  // Try out some dud values
+  std::wstring temp_key = L"";
+  EXPECT_TRUE(RegKey::GetRootKeyInfo(&temp_key) == NULL);
+  EXPECT_STREQ(temp_key.c_str(), L"");
+
+  temp_key = L"a";
+  EXPECT_TRUE(RegKey::GetRootKeyInfo(&temp_key) == NULL);
+  EXPECT_STREQ(temp_key.c_str(), L"");
+
+  // The basics
+  temp_key = L"HKLM\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_LOCAL_MACHINE);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  temp_key = L"HKEY_LOCAL_MACHINE\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_LOCAL_MACHINE);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  temp_key = L"HKCU\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CURRENT_USER);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  temp_key = L"HKEY_CURRENT_USER\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CURRENT_USER);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  temp_key = L"HKU\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_USERS);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  temp_key = L"HKEY_USERS\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_USERS);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  temp_key = L"HKCR\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CLASSES_ROOT);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  temp_key = L"HKEY_CLASSES_ROOT\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CLASSES_ROOT);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  // Make sure it is case insensitive
+  temp_key = L"hkcr\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CLASSES_ROOT);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  temp_key = L"hkey_CLASSES_ROOT\\a";
+  EXPECT_EQ(RegKey::GetRootKeyInfo(&temp_key), HKEY_CLASSES_ROOT);
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  //
+  // Test RegKey::GetParentKeyInfo
+  //
+
+  // dud cases
+  temp_key = L"";
+  EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), L"");
+  EXPECT_STREQ(temp_key.c_str(), L"");
+
+  temp_key = L"a";
+  EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), L"");
+  EXPECT_STREQ(temp_key.c_str(), L"a");
+
+  temp_key = L"a\\b";
+  EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), L"a");
+  EXPECT_STREQ(temp_key.c_str(), L"b");
+
+  temp_key = L"\\b";
+  EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(), L"");
+  EXPECT_STREQ(temp_key.c_str(), L"b");
+
+  // Some regular cases
+  temp_key = L"HKEY_CLASSES_ROOT\\moon";
+  EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(),
+               L"HKEY_CLASSES_ROOT");
+  EXPECT_STREQ(temp_key.c_str(), L"moon");
+
+  temp_key = L"HKEY_CLASSES_ROOT\\moon\\doggy";
+  EXPECT_STREQ(RegKey::GetParentKeyInfo(&temp_key).c_str(),
+               L"HKEY_CLASSES_ROOT\\moon");
+  EXPECT_STREQ(temp_key.c_str(), L"doggy");
+
+  //
+  // Test MultiSZBytesToStringArray
+  //
+
+  std::vector<std::wstring> result;
+  EXPECT_SUCCEEDED(RegKey::MultiSZBytesToStringArray(
+      reinterpret_cast<const uint8*>(kMultiSZ), sizeof(kMultiSZ), &result));
+  EXPECT_EQ(result.size(), 3);
+  EXPECT_STREQ(result[0].c_str(), L"abc");
+  EXPECT_STREQ(result[1].c_str(), L"def");
+  EXPECT_STREQ(result[2].c_str(), L"P12345");
+
+  EXPECT_SUCCEEDED(RegKey::MultiSZBytesToStringArray(
+      reinterpret_cast<const uint8*>(kEmptyMultiSZ),
+      sizeof(kEmptyMultiSZ), &result));
+  EXPECT_EQ(result.size(), 0);
+  EXPECT_FALSE(SUCCEEDED(RegKey::MultiSZBytesToStringArray(
+      reinterpret_cast<const uint8*>(kInvalidMultiSZ),
+      sizeof(kInvalidMultiSZ), &result)));
+}
+
+TEST(RegKeyTest, RegKeyHelperFunctionsTest) {
+  RegKeyHelperFunctionsTest();
+}
+
+TEST(RegKeyTest, RegKeyNonStaticFunctionsTest) {
+  DWORD int_val = 0;
+  DWORD64 int64_val = 0;
+  wchar_t* str_val = NULL;
+  uint8* binary_val = NULL;
+  DWORD uint8_count = 0;
+
+  // Just in case...
+  // make sure the no test key residue is left from previous aborted runs
+  RegKey::DeleteKey(kFullRkey1);
+
+  // initial state
+  RegKey r_key;
+  EXPECT_TRUE(r_key.key() == NULL);
+
+  // create a reg key
+  EXPECT_SUCCEEDED(r_key.Create(HKEY_CURRENT_USER, kRkey1));
+
+  // do the create twice - it should return the already created one
+  EXPECT_SUCCEEDED(r_key.Create(HKEY_CURRENT_USER, kRkey1));
+
+  // now do an open - should work just fine
+  EXPECT_SUCCEEDED(r_key.Open(HKEY_CURRENT_USER, kRkey1));
+
+  // get an in-existent value
+  EXPECT_EQ(r_key.GetValue(kValNameInt, &int_val),
+            HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
+
+  // set and get some values
+
+  // set an INT 32
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt, kIntVal));
+
+  // check that the value exists
+  EXPECT_TRUE(r_key.HasValue(kValNameInt));
+
+  // read it back
+  EXPECT_SUCCEEDED(r_key.GetValue(kValNameInt, &int_val));
+  EXPECT_EQ(int_val, kIntVal);
+
+  // set it again!
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt, kIntVal2));
+
+  // read it again
+  EXPECT_SUCCEEDED(r_key.GetValue(kValNameInt, &int_val));
+  EXPECT_EQ(int_val, kIntVal2);
+
+  // delete the value
+  EXPECT_SUCCEEDED(r_key.DeleteValue(kValNameInt));
+
+  // check that the value is gone
+  EXPECT_FALSE(r_key.HasValue(kValNameInt));
+
+  // set an INT 64
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt64, kIntVal64));
+
+  // check that the value exists
+  EXPECT_TRUE(r_key.HasValue(kValNameInt64));
+
+  // read it back
+  EXPECT_SUCCEEDED(r_key.GetValue(kValNameInt64, &int64_val));
+  EXPECT_EQ(int64_val, kIntVal64);
+
+  // delete the value
+  EXPECT_SUCCEEDED(r_key.DeleteValue(kValNameInt64));
+
+  // check that the value is gone
+  EXPECT_FALSE(r_key.HasValue(kValNameInt64));
+
+  // set a string
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameStr, kStrVal));
+
+  // check that the value exists
+  EXPECT_TRUE(r_key.HasValue(kValNameStr));
+
+  // read it back
+  EXPECT_SUCCEEDED(r_key.GetValue(kValNameStr, &str_val));
+  EXPECT_TRUE(lstrcmp(str_val, kStrVal) == 0);
+  delete[] str_val;
+
+  // set it again
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameStr, kStrVal2));
+
+  // read it again
+  EXPECT_SUCCEEDED(r_key.GetValue(kValNameStr, &str_val));
+  EXPECT_TRUE(lstrcmp(str_val, kStrVal2) == 0);
+  delete[] str_val;
+
+  // delete the value
+  EXPECT_SUCCEEDED(r_key.DeleteValue(kValNameStr));
+
+  // check that the value is gone
+  EXPECT_FALSE(r_key.HasValue(kValNameInt));
+
+  // set a binary value
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameBinary,
+      reinterpret_cast<const uint8*>(kBinaryVal), sizeof(kBinaryVal) - 1));
+
+  // check that the value exists
+  EXPECT_TRUE(r_key.HasValue(kValNameBinary));
+
+  // read it back
+  EXPECT_SUCCEEDED(r_key.GetValue(kValNameBinary, &binary_val, &uint8_count));
+  EXPECT_TRUE(memcmp(binary_val, kBinaryVal, sizeof(kBinaryVal) - 1) == 0);
+  delete[] binary_val;
+
+  // set it again
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameBinary,
+      reinterpret_cast<const uint8*>(kBinaryVal2), sizeof(kBinaryVal) - 1));
+
+  // read it again
+  EXPECT_SUCCEEDED(r_key.GetValue(kValNameBinary, &binary_val, &uint8_count));
+  EXPECT_TRUE(memcmp(binary_val, kBinaryVal2, sizeof(kBinaryVal2) - 1) == 0);
+  delete[] binary_val;
+
+  // delete the value
+  EXPECT_SUCCEEDED(r_key.DeleteValue(kValNameBinary));
+
+  // check that the value is gone
+  EXPECT_FALSE(r_key.HasValue(kValNameBinary));
+
+  // set some values and check the total count
+
+  // set an INT 32
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt, kIntVal));
+
+  // set an INT 64
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameInt64, kIntVal64));
+
+  // set a string
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameStr, kStrVal));
+
+  // set a binary value
+  EXPECT_SUCCEEDED(r_key.SetValue(kValNameBinary,
+      reinterpret_cast<const uint8*>(kBinaryVal), sizeof(kBinaryVal) - 1));
+
+  // get the value count
+  uint32 value_count = r_key.GetValueCount();
+  EXPECT_EQ(value_count, 4);
+
+  // check the value names
+  std::wstring value_name;
+  DWORD type = 0;
+
+  EXPECT_SUCCEEDED(r_key.GetValueNameAt(0, &value_name, &type));
+  EXPECT_STREQ(value_name.c_str(), kValNameInt);
+  EXPECT_EQ(type, REG_DWORD);
+
+  EXPECT_SUCCEEDED(r_key.GetValueNameAt(1, &value_name, &type));
+  EXPECT_STREQ(value_name.c_str(), kValNameInt64);
+  EXPECT_EQ(type, REG_QWORD);
+
+  EXPECT_SUCCEEDED(r_key.GetValueNameAt(2, &value_name, &type));
+  EXPECT_STREQ(value_name.c_str(), kValNameStr);
+  EXPECT_EQ(type, REG_SZ);
+
+  EXPECT_SUCCEEDED(r_key.GetValueNameAt(3, &value_name, &type));
+  EXPECT_STREQ(value_name.c_str(), kValNameBinary);
+  EXPECT_EQ(type, REG_BINARY);
+
+  // check that there are no more values
+  EXPECT_FAILED(r_key.GetValueNameAt(4, &value_name, &type));
+
+  uint32 subkey_count = r_key.GetSubkeyCount();
+  EXPECT_EQ(subkey_count, 0);
+
+  // now create a subkey and make sure we can get the name
+  RegKey temp_key;
+  EXPECT_SUCCEEDED(temp_key.Create(HKEY_CURRENT_USER, kRkey1Subkey));
+
+  // check the subkey exists
+  EXPECT_TRUE(r_key.HasSubkey(kRkey1SubkeyName));
+
+  // check the name
+  EXPECT_EQ(r_key.GetSubkeyCount(), 1);
+
+  std::wstring subkey_name;
+  EXPECT_SUCCEEDED(r_key.GetSubkeyNameAt(0, &subkey_name));
+  EXPECT_STREQ(subkey_name.c_str(), kRkey1SubkeyName);
+
+  // delete the key
+  EXPECT_SUCCEEDED(r_key.DeleteSubKey(kRkey1));
+
+  // close this key
+  EXPECT_SUCCEEDED(r_key.Close());
+
+  // whack the whole key
+  EXPECT_SUCCEEDED(RegKey::DeleteKey(kFullRkey1));
+}
+
+TEST(RegKeyTest, RegKeyStaticFunctionsTest) {
+  DWORD int_val = 0;
+  DWORD64 int64_val = 0;
+  float float_val = 0;
+  double double_val = 0;
+  wchar_t* str_val = NULL;
+  std::wstring wstr_val;
+  uint8* binary_val = NULL;
+  DWORD uint8_count = 0;
+
+  // Just in case...
+  // make sure the no test key residue is left from previous aborted runs
+  RegKey::DeleteKey(kFullRkey1);
+
+  // get an in-existent value from an un-existent key
+  EXPECT_EQ(RegKey::GetValue(kFullRkey1, kValNameInt, &int_val),
+            HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
+
+  // set int32
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameInt, kIntVal));
+
+  // check that the value exists
+  EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameInt));
+
+  // get an in-existent value from an existent key
+  EXPECT_EQ(RegKey::GetValue(kFullRkey1, L"bogus", &int_val),
+            HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
+
+  // read it back
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameInt, &int_val));
+  EXPECT_EQ(int_val, kIntVal);
+
+  // delete the value
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameInt));
+
+  // check that the value is gone
+  EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameInt));
+
+  // set int64
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameInt64, kIntVal64));
+
+  // check that the value exists
+  EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameInt64));
+
+  // read it back
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameInt64, &int64_val));
+  EXPECT_EQ(int64_val, kIntVal64);
+
+  // delete the value
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameInt64));
+
+  // check that the value is gone
+  EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameInt64));
+
+  // set float
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameFloat, kFloatVal));
+
+  // check that the value exists
+  EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameFloat));
+
+  // read it back
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameFloat, &float_val));
+  EXPECT_EQ(float_val, kFloatVal);
+
+  // delete the value
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameFloat));
+
+  // check that the value is gone
+  EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameFloat));
+  EXPECT_FAILED(RegKey::GetValue(kFullRkey1, kValNameFloat, &float_val));
+
+  // set double
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameDouble, kDoubleVal));
+
+  // check that the value exists
+  EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameDouble));
+
+  // read it back
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameDouble, &double_val));
+  EXPECT_EQ(double_val, kDoubleVal);
+
+  // delete the value
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameDouble));
+
+  // check that the value is gone
+  EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameDouble));
+  EXPECT_FAILED(RegKey::GetValue(kFullRkey1, kValNameDouble, &double_val));
+
+  // set string
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameStr, kStrVal));
+
+  // check that the value exists
+  EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameStr));
+
+  // read it back
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameStr, &str_val));
+  EXPECT_TRUE(lstrcmp(str_val, kStrVal) == 0);
+  delete[] str_val;
+
+  // read it back in std::wstring
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameStr, &wstr_val));
+  EXPECT_STREQ(wstr_val.c_str(), kStrVal);
+
+  // get an in-existent value from an existent key
+  EXPECT_EQ(RegKey::GetValue(kFullRkey1, L"bogus", &str_val),
+            HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
+
+  // delete the value
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameStr));
+
+  // check that the value is gone
+  EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameStr));
+
+  // set binary
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameBinary,
+      reinterpret_cast<const uint8*>(kBinaryVal), sizeof(kBinaryVal)-1));
+
+  // check that the value exists
+  EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameBinary));
+
+  // read it back
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameBinary,
+      &binary_val, &uint8_count));
+  EXPECT_TRUE(memcmp(binary_val, kBinaryVal, sizeof(kBinaryVal)-1) == 0);
+  delete[] binary_val;
+
+  // delete the value
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameBinary));
+
+  // check that the value is gone
+  EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameBinary));
+
+  // special case - set a binary value with length 0
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameBinary,
+      reinterpret_cast<const uint8*>(kBinaryVal), 0));
+
+  // check that the value exists
+  EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameBinary));
+
+  // read it back
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameBinary,
+      &binary_val, &uint8_count));
+  EXPECT_EQ(uint8_count, 0);
+  EXPECT_TRUE(binary_val == NULL);
+  delete[] binary_val;
+
+  // delete the value
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameBinary));
+
+  // check that the value is gone
+  EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameBinary));
+
+  // special case - set a NULL binary value
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1, kValNameBinary, NULL, 100));
+
+  // check that the value exists
+  EXPECT_TRUE(RegKey::HasValue(kFullRkey1, kValNameBinary));
+
+  // read it back
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameBinary,
+                                    &binary_val, &uint8_count));
+  EXPECT_EQ(uint8_count, 0);
+  EXPECT_TRUE(binary_val == NULL);
+  delete[] binary_val;
+
+  // delete the value
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1, kValNameBinary));
+
+  // check that the value is gone
+  EXPECT_FALSE(RegKey::HasValue(kFullRkey1, kValNameBinary));
+
+  // test read/write REG_MULTI_SZ value
+  std::vector<std::wstring> result;
+  EXPECT_SUCCEEDED(RegKey::SetValueMultiSZ(kFullRkey1, kValNameMultiStr,
+      reinterpret_cast<const uint8*>(kMultiSZ), sizeof(kMultiSZ)));
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameMultiStr, &result));
+  EXPECT_EQ(result.size(), 3);
+  EXPECT_STREQ(result[0].c_str(), L"abc");
+  EXPECT_STREQ(result[1].c_str(), L"def");
+  EXPECT_STREQ(result[2].c_str(), L"P12345");
+  EXPECT_SUCCEEDED(RegKey::SetValueMultiSZ(kFullRkey1, kValNameMultiStr,
+      reinterpret_cast<const uint8*>(kEmptyMultiSZ), sizeof(kEmptyMultiSZ)));
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameMultiStr, &result));
+  EXPECT_EQ(result.size(), 0);
+  // writing REG_MULTI_SZ value will automatically add ending null characters
+  EXPECT_SUCCEEDED(RegKey::SetValueMultiSZ(kFullRkey1, kValNameMultiStr,
+      reinterpret_cast<const uint8*>(kInvalidMultiSZ), sizeof(kInvalidMultiSZ)));
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1, kValNameMultiStr, &result));
+  EXPECT_EQ(result.size(), 1);
+  EXPECT_STREQ(result[0].c_str(), L"678");
+
+  // Run the following test only in dev machine
+  // This is because the build machine might not have admin privilege
+#ifdef IS_PRIVATE_BUILD
+  // get a temp file name
+  wchar_t temp_path[MAX_PATH] = {0};
+  EXPECT_LT(::GetTempPath(ARRAY_SIZE(temp_path), temp_path),
+            static_cast<DWORD>(ARRAY_SIZE(temp_path)));
+  wchar_t temp_file[MAX_PATH] = {0};
+  EXPECT_NE(::GetTempFileName(temp_path, L"rkut_",
+                              ::GetTickCount(), temp_file), 0);
+
+  // test save
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1Subkey, kValNameInt, kIntVal));
+  EXPECT_SUCCEEDED(RegKey::SetValue(kFullRkey1Subkey, kValNameInt64, kIntVal64));
+  EXPECT_SUCCEEDED(RegKey::Save(kFullRkey1Subkey, temp_file));
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1Subkey, kValNameInt));
+  EXPECT_SUCCEEDED(RegKey::DeleteValue(kFullRkey1Subkey, kValNameInt64));
+
+  // test restore
+  EXPECT_SUCCEEDED(RegKey::Restore(kFullRkey1Subkey, temp_file));
+  int_val = 0;
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1Subkey, kValNameInt, &int_val));
+  EXPECT_EQ(int_val, kIntVal);
+  int64_val = 0;
+  EXPECT_SUCCEEDED(RegKey::GetValue(kFullRkey1Subkey,
+                                    kValNameInt64,
+                                    &int64_val));
+  EXPECT_EQ(int64_val, kIntVal64);
+
+  // delete the temp file
+  EXPECT_EQ(TRUE, ::DeleteFile(temp_file));
+#endif
+
+  // whack the whole key
+  EXPECT_SUCCEEDED(RegKey::DeleteKey(kFullRkey1));
+}
+
+}  // namespace talk_base
diff --git a/talk/base/win32socketserver_unittest.cc b/talk/base/win32socketserver_unittest.cc
new file mode 100644
index 0000000..8531861
--- /dev/null
+++ b/talk/base/win32socketserver_unittest.cc
@@ -0,0 +1,91 @@
+// Copyright 2009 Google Inc. All Rights Reserved.
+
+
+#include "talk/base/gunit.h"
+#include "talk/base/socket_unittest.h"
+#include "talk/base/thread.h"
+#include "talk/base/win32socketserver.h"
+
+namespace talk_base {
+
+// Test that Win32SocketServer::Wait works as expected.
+TEST(Win32SocketServerTest, TestWait) {
+  Win32SocketServer server(NULL);
+  uint32 start = Time();
+  server.Wait(1000, true);
+  EXPECT_GE(TimeSince(start), 1000);
+}
+
+// Test that Win32Socket::Pump does not touch general Windows messages.
+TEST(Win32SocketServerTest, TestPump) {
+  Win32SocketServer server(NULL);
+  SocketServerScope scope(&server);
+  EXPECT_EQ(TRUE, PostMessage(NULL, WM_USER, 999, 0));
+  server.Pump();
+  MSG msg;
+  EXPECT_EQ(TRUE, PeekMessage(&msg, NULL, 0, 0, PM_REMOVE));
+  EXPECT_EQ(WM_USER, msg.message);
+  EXPECT_EQ(999, msg.wParam);
+}
+
+// Test that Win32Socket passes all the generic Socket tests.
+class Win32SocketTest : public SocketTest {
+ protected:
+  Win32SocketTest() : server_(NULL), scope_(&server_) {}
+  Win32SocketServer server_;
+  SocketServerScope scope_;
+};
+
+TEST_F(Win32SocketTest, TestConnect) {
+  SocketTest::TestConnect();
+}
+
+TEST_F(Win32SocketTest, TestConnectWithDnsLookup) {
+  SocketTest::TestConnectWithDnsLookup();
+}
+
+TEST_F(Win32SocketTest, TestConnectFail) {
+  SocketTest::TestConnectFail();
+}
+
+TEST_F(Win32SocketTest, TestConnectWithDnsLookupFail) {
+  SocketTest::TestConnectWithDnsLookupFail();
+}
+
+TEST_F(Win32SocketTest, TestConnectWithClosedSocket) {
+  SocketTest::TestConnectWithClosedSocket();
+}
+
+TEST_F(Win32SocketTest, TestServerCloseDuringConnect) {
+  SocketTest::TestServerCloseDuringConnect();
+}
+
+TEST_F(Win32SocketTest, TestClientCloseDuringConnect) {
+  SocketTest::TestClientCloseDuringConnect();
+}
+
+TEST_F(Win32SocketTest, TestServerClose) {
+  SocketTest::TestServerClose();
+}
+
+TEST_F(Win32SocketTest, TestCloseInClosedCallback) {
+  SocketTest::TestCloseInClosedCallback();
+}
+
+TEST_F(Win32SocketTest, TestSocketServerWait) {
+  SocketTest::TestSocketServerWait();
+}
+
+TEST_F(Win32SocketTest, TestTcp) {
+  SocketTest::TestTcp();
+}
+
+TEST_F(Win32SocketTest, TestUdp) {
+  SocketTest::TestUdp();
+}
+
+TEST_F(Win32SocketTest, TestGetSetOptions) {
+  SocketTest::TestGetSetOptions();
+}
+
+}  // namespace talk_base
diff --git a/talk/base/win32toolhelp_unittest.cc b/talk/base/win32toolhelp_unittest.cc
new file mode 100644
index 0000000..529bef9
--- /dev/null
+++ b/talk/base/win32toolhelp_unittest.cc
@@ -0,0 +1,296 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#include "talk/base/gunit.h"
+#include "talk/base/pathutils.h"
+#include "talk/base/scoped_ptr.h"
+#include "talk/base/win32toolhelp.h"
+
+namespace talk_base {
+
+typedef struct {
+  // Required to match the toolhelp api struct 'design'.
+  DWORD dwSize;
+  int a;
+  uint32 b;
+} TestData;
+
+class Win32ToolhelpTest : public testing::Test {
+ public:
+  Win32ToolhelpTest() {
+  }
+
+  HANDLE AsHandle() {
+    return reinterpret_cast<HANDLE>(this);
+  }
+
+  static Win32ToolhelpTest* AsFixture(HANDLE handle) {
+    return reinterpret_cast<Win32ToolhelpTest*>(handle);
+  }
+
+  static bool First(HANDLE handle, TestData* d) {
+    Win32ToolhelpTest* tst = Win32ToolhelpTest::AsFixture(handle);
+    // This method should be called only once for every test.
+    // If it is called more than once it return false which
+    // should break the test.
+    EXPECT_EQ(0, tst->first_called_); // Just to be safe.
+    if (tst->first_called_ > 0) {
+      return false;
+    }
+
+    *d = kTestData[0];
+    tst->index_ = 1;
+    ++(tst->first_called_);
+    return true;
+  }
+
+  static bool Next(HANDLE handle, TestData* d) {
+    Win32ToolhelpTest* tst = Win32ToolhelpTest::AsFixture(handle);
+    ++(tst->next_called_);
+
+    if (tst->index_ >= kTestDataSize) {
+      return FALSE;
+    }
+
+    *d = kTestData[tst->index_];
+    ++(tst->index_);
+    return true;
+  }
+
+  static bool Fail(HANDLE handle, TestData* d) {
+    Win32ToolhelpTest* tst = Win32ToolhelpTest::AsFixture(handle);
+    ++(tst->fail_called_);
+    return false;
+  }
+
+  static bool CloseHandle(HANDLE handle) {
+    Win32ToolhelpTest* tst = Win32ToolhelpTest::AsFixture(handle);
+    ++(tst->close_handle_called_);
+    return true;
+  }
+
+ protected:
+  virtual void SetUp() {
+    fail_called_ = 0;
+    first_called_ = 0;
+    next_called_ = 0;
+    close_handle_called_ = 0;
+    index_ = 0;
+  }
+
+  static bool AllZero(const TestData& data) {
+    return data.dwSize == 0 && data.a == 0 && data.b == 0;
+  }
+
+  static bool Equals(const TestData& expected, const TestData& actual) {
+    return expected.dwSize == actual.dwSize
+        && expected.a == actual.a
+        && expected.b == actual.b;
+  }
+
+  bool CheckCallCounters(int first, int next, int fail, int close) {
+    bool match = first_called_ == first && next_called_ == next
+      && fail_called_ == fail && close_handle_called_ == close;
+
+    if (!match) {
+      LOG(LS_ERROR) << "Expected: ("
+                    << first << ", "
+                    << next << ", "
+                    << fail << ", "
+                    << close << ")";
+
+      LOG(LS_ERROR) << "Actual: ("
+                    << first_called_ << ", "
+                    << next_called_ << ", "
+                    << fail_called_ << ", "
+                    << close_handle_called_ << ")";
+    }
+    return match;
+  }
+
+  static const int kTestDataSize = 3;
+  static const TestData kTestData[];
+  int index_;
+  int first_called_;
+  int fail_called_;
+  int next_called_;
+  int close_handle_called_;
+};
+
+const TestData Win32ToolhelpTest::kTestData[] = {
+  {1, 1, 1}, {2, 2, 2}, {3, 3, 3}
+};
+
+
+class TestTraits {
+ public:
+  typedef TestData Type;
+
+  static bool First(HANDLE handle, Type* t) {
+    return Win32ToolhelpTest::First(handle, t);
+  }
+
+  static bool Next(HANDLE handle, Type* t) {
+    return Win32ToolhelpTest::Next(handle, t);
+  }
+
+  static bool CloseHandle(HANDLE handle) {
+    return Win32ToolhelpTest::CloseHandle(handle);
+  }
+};
+
+class BadFirstTraits {
+ public:
+  typedef TestData Type;
+
+  static bool First(HANDLE handle, Type* t) {
+    return Win32ToolhelpTest::Fail(handle, t);
+  }
+
+  static bool Next(HANDLE handle, Type* t) {
+    // This should never be called.
+    ADD_FAILURE();
+    return false;
+  }
+
+  static bool CloseHandle(HANDLE handle) {
+    return Win32ToolhelpTest::CloseHandle(handle);
+  }
+};
+
+class BadNextTraits {
+ public:
+  typedef TestData Type;
+
+  static bool First(HANDLE handle, Type* t) {
+    return Win32ToolhelpTest::First(handle, t);
+  }
+
+  static bool Next(HANDLE handle, Type* t) {
+    return Win32ToolhelpTest::Fail(handle, t);
+  }
+
+  static bool CloseHandle(HANDLE handle) {
+    return Win32ToolhelpTest::CloseHandle(handle);
+  }
+};
+
+// The toolhelp in normally inherited but most of
+// these tests only excercise the methods from the
+// traits therefore I use a typedef to make the
+// test code easier to read.
+typedef talk_base::ToolhelpEnumeratorBase<TestTraits> EnumeratorForTest;
+
+TEST_F(Win32ToolhelpTest, TestNextWithInvalidCtorHandle) {
+  EnumeratorForTest t(INVALID_HANDLE_VALUE);
+
+  EXPECT_FALSE(t.Next());
+  EXPECT_TRUE(CheckCallCounters(0, 0, 0, 0));
+}
+
+// Tests that Next() returns false if the first-pointer
+// function fails.
+TEST_F(Win32ToolhelpTest, TestNextFirstFails) {
+  typedef talk_base::ToolhelpEnumeratorBase<BadFirstTraits> BadEnumerator;
+  talk_base::scoped_ptr<BadEnumerator> t(new BadEnumerator(AsHandle()));
+
+  // If next ever fails it shall always fail.
+  EXPECT_FALSE(t->Next());
+  EXPECT_FALSE(t->Next());
+  EXPECT_FALSE(t->Next());
+  t.reset();
+  EXPECT_TRUE(CheckCallCounters(0, 0, 1, 1));
+}
+
+// Tests that Next() returns false if the next-pointer
+// function fails.
+TEST_F(Win32ToolhelpTest, TestNextNextFails) {
+  typedef talk_base::ToolhelpEnumeratorBase<BadNextTraits> BadEnumerator;
+  talk_base::scoped_ptr<BadEnumerator> t(new BadEnumerator(AsHandle()));
+
+  // If next ever fails it shall always fail. No more calls
+  // shall be dispatched to Next(...).
+  EXPECT_TRUE(t->Next());
+  EXPECT_FALSE(t->Next());
+  EXPECT_FALSE(t->Next());
+  t.reset();
+  EXPECT_TRUE(CheckCallCounters(1, 0, 1, 1));
+}
+
+
+// Tests that current returns an object is all zero's
+// if Next() hasn't been called.
+TEST_F(Win32ToolhelpTest, TestCurrentNextNotCalled) {
+  talk_base::scoped_ptr<EnumeratorForTest> t(new EnumeratorForTest(AsHandle()));
+  EXPECT_TRUE(AllZero(t->current()));
+  t.reset();
+  EXPECT_TRUE(CheckCallCounters(0, 0, 0, 1));
+}
+
+// Tests the simple everything works path through the code.
+TEST_F(Win32ToolhelpTest, TestCurrentNextCalled) {
+  talk_base::scoped_ptr<EnumeratorForTest> t(new EnumeratorForTest(AsHandle()));
+
+  EXPECT_TRUE(t->Next());
+  EXPECT_TRUE(Equals(t->current(), kTestData[0]));
+  EXPECT_TRUE(t->Next());
+  EXPECT_TRUE(Equals(t->current(), kTestData[1]));
+  EXPECT_TRUE(t->Next());
+  EXPECT_TRUE(Equals(t->current(), kTestData[2]));
+  EXPECT_FALSE(t->Next());
+  t.reset();
+  EXPECT_TRUE(CheckCallCounters(1, 3, 0, 1));
+}
+
+TEST_F(Win32ToolhelpTest, TestCurrentProcess) {
+  int size = MAX_PATH;
+  WCHAR buf[MAX_PATH];
+  GetModuleFileName(NULL, buf, ARRAY_SIZE(buf));
+  std::wstring name = ToUtf16(Pathname(ToUtf8(buf)).filename());
+
+  talk_base::ProcessEnumerator processes;
+  bool found = false;
+  while (processes.Next()) {
+    if (!name.compare(processes.current().szExeFile)) {
+      found = true;
+      break;
+    }
+  }
+  EXPECT_TRUE(found);
+
+  talk_base::ModuleEnumerator modules(processes.current().th32ProcessID);
+  found = false;
+  while (modules.Next()) {
+    if (!name.compare(modules.current().szModule)) {
+      found = true;
+      break;
+    }
+  }
+  EXPECT_TRUE(found);
+}
+
+}  // namespace talk_base
diff --git a/talk/base/win32window_unittest.cc b/talk/base/win32window_unittest.cc
new file mode 100644
index 0000000..96173b7
--- /dev/null
+++ b/talk/base/win32window_unittest.cc
@@ -0,0 +1,83 @@
+/*
+ * libjingle
+ * Copyright 2009, 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.
+ */
+
+#include "talk/base/gunit.h"
+#include "talk/base/common.h"
+#include "talk/base/win32window.h"
+#include "talk/base/logging.h"
+
+static LRESULT kDummyResult = 0x1234ABCD;
+
+class TestWindow : public talk_base::Win32Window {
+ public:
+  TestWindow() : destroyed_(false) { memset(&msg_, 0, sizeof(msg_)); }
+  const MSG& msg() const { return msg_; }
+  bool destroyed() const { return destroyed_; }
+
+  virtual bool OnMessage(UINT uMsg, WPARAM wParam,
+                         LPARAM lParam, LRESULT& result) {
+    msg_.message = uMsg;
+    msg_.wParam = wParam;
+    msg_.lParam = lParam;
+    result = kDummyResult;
+    return true;
+  }
+  virtual void OnNcDestroy() {
+    destroyed_ = true;
+  }
+
+ private:
+  MSG msg_;
+  bool destroyed_;
+};
+
+TEST(Win32WindowTest, Basics) {
+  TestWindow wnd;
+  EXPECT_TRUE(wnd.handle() == NULL);
+  EXPECT_FALSE(wnd.destroyed());
+  EXPECT_TRUE(wnd.Create(0, L"Test", 0, 0, 0, 0, 100, 100));
+  EXPECT_TRUE(wnd.handle() != NULL);
+  EXPECT_EQ(kDummyResult, ::SendMessage(wnd.handle(), WM_USER, 1, 2));
+  EXPECT_EQ(WM_USER, wnd.msg().message);
+  EXPECT_EQ(1, wnd.msg().wParam);
+  EXPECT_EQ(2, wnd.msg().lParam);
+  wnd.Destroy();
+  EXPECT_TRUE(wnd.handle() == NULL);
+  EXPECT_TRUE(wnd.destroyed());
+}
+
+TEST(Win32WindowTest, MultipleWindows) {
+  TestWindow wnd1, wnd2;
+  EXPECT_TRUE(wnd1.Create(0, L"Test", 0, 0, 0, 0, 100, 100));
+  EXPECT_TRUE(wnd2.Create(0, L"Test", 0, 0, 0, 0, 100, 100));
+  EXPECT_TRUE(wnd1.handle() != NULL);
+  EXPECT_TRUE(wnd2.handle() != NULL);
+  wnd1.Destroy();
+  wnd2.Destroy();
+  EXPECT_TRUE(wnd2.handle() == NULL);
+  EXPECT_TRUE(wnd1.handle() == NULL);
+}
diff --git a/talk/base/win32windowpicker.cc b/talk/base/win32windowpicker.cc
new file mode 100644
index 0000000..308fff5
--- /dev/null
+++ b/talk/base/win32windowpicker.cc
@@ -0,0 +1,78 @@
+// Copyright 2010 Google Inc. All Rights Reserved
+
+
+#include "talk/base/win32windowpicker.h"
+
+#include <string>
+
+#include "talk/base/common.h"
+#include "talk/base/logging.h"
+
+namespace talk_base {
+
+// Window class names that we want to filter out.
+static const std::string kProgramManagerClass = "Progman";
+static const std::string kButtonClass = "Button";
+
+BOOL CALLBACK Win32WindowPicker::EnumProc(HWND hwnd, LPARAM l_param) {
+  WindowDescriptionList* descriptions =
+      reinterpret_cast<WindowDescriptionList*>(l_param);
+
+  // Skip windows that are invisible, minimized, have no title, or are owned,
+  // unless they have the app window style set. Except for minimized windows,
+  // this is what Alt-Tab does.
+  // TODO: Figure out how to grab a thumbnail of a minimized window and
+  // include them in the list.
+  int len = GetWindowTextLength(hwnd);
+  HWND owner = GetWindow(hwnd, GW_OWNER);
+  LONG exstyle = GetWindowLong(hwnd, GWL_EXSTYLE);
+  if (len == 0 || IsIconic(hwnd) || !IsWindowVisible(hwnd) ||
+      (owner && !(exstyle & WS_EX_APPWINDOW))) {
+    // TODO: Investigate if windows without title still could be
+    // interesting to share. We could use the name of the process as title:
+    //
+    // GetWindowThreadProcessId()
+    // OpenProcess()
+    // QueryFullProcessImageName()
+    return TRUE;
+  }
+
+  // Skip the Program Manager window and the Start button.
+  TCHAR class_name_w[500];
+  ::GetClassName(hwnd, class_name_w, 500);
+  std::string class_name = ToUtf8(class_name_w, wcslen(class_name_w));
+  if (class_name == kProgramManagerClass || class_name == kButtonClass) {
+    // We don't want the Program Manager window nor the Start button.
+    return TRUE;
+  }
+
+  TCHAR window_title[500];
+  GetWindowText(hwnd, window_title, ARRAY_SIZE(window_title));
+  std::string title = ToUtf8(window_title, wcslen(window_title));
+  WindowDescription desc(reinterpret_cast<int>(hwnd), title);
+  descriptions->push_back(desc);
+  return TRUE;
+}
+
+Win32WindowPicker::Win32WindowPicker() {
+}
+
+bool Win32WindowPicker::Init() {
+  return true;
+}
+
+bool Win32WindowPicker::GetWindowList(WindowDescriptionList* descriptions) {
+  return EnumWindows(Win32WindowPicker::EnumProc,
+                     (LPARAM)descriptions) == TRUE ? true : false;
+}
+
+bool Win32WindowPicker::IsVisible(WindowId id) {
+  HWND hwnd = reinterpret_cast<HWND>(id);
+  return (::IsWindow(hwnd) != FALSE && ::IsWindowVisible(hwnd) != FALSE);
+}
+
+bool Win32WindowPicker::MoveToFront(WindowId id) {
+  return SetForegroundWindow(reinterpret_cast<HWND>(id)) != FALSE;
+}
+
+}  // namespace talk_base
diff --git a/talk/base/win32windowpicker.h b/talk/base/win32windowpicker.h
new file mode 100644
index 0000000..d799037
--- /dev/null
+++ b/talk/base/win32windowpicker.h
@@ -0,0 +1,26 @@
+// Copyright 2010 Google Inc. All Rights Reserved
+
+
+#ifndef TALK_BASE_WIN32WINDOWPICKER_H_
+#define TALK_BASE_WIN32WINDOWPICKER_H_
+
+#include "talk/base/win32.h"
+#include "talk/base/windowpicker.h"
+
+namespace talk_base {
+
+class Win32WindowPicker : public WindowPicker {
+ public:
+  Win32WindowPicker();
+  virtual bool Init();
+  virtual bool IsVisible(WindowId id);
+  virtual bool MoveToFront(WindowId id);
+  virtual bool GetWindowList(WindowDescriptionList* descriptions);
+
+ protected:
+  static BOOL CALLBACK EnumProc(HWND hwnd, LPARAM l_param);
+};
+
+}  // namespace talk_base
+
+#endif  // TALK_BASE_WIN32WINDOWPICKER_H_
diff --git a/talk/base/win32windowpicker_unittest.cc b/talk/base/win32windowpicker_unittest.cc
new file mode 100644
index 0000000..d6ee553
--- /dev/null
+++ b/talk/base/win32windowpicker_unittest.cc
@@ -0,0 +1,99 @@
+// Copyright 2010 Google Inc. All Rights Reserved
+
+
+#include "talk/base/gunit.h"
+#include "talk/base/common.h"
+#include "talk/base/logging.h"
+#include "talk/base/win32window.h"
+#include "talk/base/win32windowpicker.h"
+#include "talk/base/windowpicker.h"
+
+#ifndef WIN32
+#error Only for Windows
+#endif
+
+namespace talk_base {
+
+static const TCHAR* kVisibleWindowTitle = L"Visible Window";
+static const TCHAR* kInvisibleWindowTitle = L"Invisible Window";
+
+class Win32WindowPickerForTest : public Win32WindowPicker {
+ public:
+  Win32WindowPickerForTest() {
+    EXPECT_TRUE(visible_window_.Create(NULL, kVisibleWindowTitle, WS_VISIBLE,
+                                       0, 0, 0, 0, 0));
+    EXPECT_TRUE(invisible_window_.Create(NULL, kInvisibleWindowTitle, 0,
+                                         0, 0, 0, 0, 0));
+  }
+
+  ~Win32WindowPickerForTest() {
+    visible_window_.Destroy();
+    invisible_window_.Destroy();
+  }
+
+  virtual bool GetWindowList(WindowDescriptionList* descriptions) {
+    if (!Win32WindowPicker::EnumProc(visible_window_.handle(),
+                                     reinterpret_cast<LPARAM>(descriptions))) {
+      return false;
+    }
+    if (!Win32WindowPicker::EnumProc(invisible_window_.handle(),
+                                     reinterpret_cast<LPARAM>(descriptions))) {
+      return false;
+    }
+    return true;
+  }
+
+  Win32Window* visible_window() {
+    return &visible_window_;
+  }
+
+  Win32Window* invisible_window() {
+    return &invisible_window_;
+  }
+
+ private:
+  Win32Window visible_window_;
+  Win32Window invisible_window_;
+};
+
+TEST(Win32WindowPickerTest, TestGetWindowList) {
+  Win32WindowPickerForTest window_picker;
+  WindowDescriptionList descriptions;
+  EXPECT_TRUE(window_picker.GetWindowList(&descriptions));
+  EXPECT_EQ(1, descriptions.size());
+  WindowDescription desc = descriptions.front();
+  EXPECT_EQ(reinterpret_cast<WindowId>(
+                window_picker.visible_window()->handle()), desc.id());
+  TCHAR window_title[500];
+  GetWindowText(window_picker.visible_window()->handle(), window_title,
+                ARRAY_SIZE(window_title));
+  EXPECT_EQ(ToUtf8(window_title, wcslen(window_title)),
+            ToUtf8(kVisibleWindowTitle, wcslen(kVisibleWindowTitle)));
+}
+
+TEST(Win32WindowPickerTest, TestIsVisible) {
+  Win32WindowPickerForTest window_picker;
+  WindowId visible_id =
+      reinterpret_cast<WindowId>(window_picker.visible_window()->handle());
+  WindowId invisible_id =
+      reinterpret_cast<WindowId>(window_picker.invisible_window()->handle());
+  EXPECT_TRUE(window_picker.IsVisible(visible_id));
+  EXPECT_FALSE(window_picker.IsVisible(invisible_id));
+}
+
+TEST(Win32WindowPickerTest, TestMoveToFront) {
+  Win32WindowPickerForTest window_picker;
+  WindowId visible_id =
+      reinterpret_cast<WindowId>(window_picker.visible_window()->handle());
+  WindowId invisible_id =
+      reinterpret_cast<WindowId>(window_picker.invisible_window()->handle());
+
+  // There are a number of condition where SetForegroundWindow might
+  // fail depending on the state of the calling process. To be on the
+  // safe side we doesn't expect MoveToFront to return true, just test
+  // that we don't crash.
+  window_picker.MoveToFront(visible_id);
+  window_picker.MoveToFront(invisible_id);
+}
+
+}  // namespace talk_base
diff --git a/talk/base/windowpicker.h b/talk/base/windowpicker.h
new file mode 100644
index 0000000..2de7041
--- /dev/null
+++ b/talk/base/windowpicker.h
@@ -0,0 +1,52 @@
+// Copyright 2010 Google Inc. All Rights Reserved
+
+//         thorcarpenter@google.com (Thor Carpenter)
+
+#ifndef TALK_BASE_WINDOWPICKER_H_
+#define TALK_BASE_WINDOWPICKER_H_
+
+#include <list>
+#include <string>
+
+#include "talk/base/window.h"
+
+namespace talk_base {
+
+class WindowDescription {
+ public:
+  WindowDescription() : id_(kInvalidWindowId) {}
+  WindowDescription(WindowId id, const std::string& title)
+      : id_(id), title_(title) {
+  }
+  WindowId id() const {
+    return id_;
+  }
+  const std::string& title() const {
+    return title_;
+  }
+
+ private:
+  WindowId id_;
+  std::string title_;
+};
+
+typedef std::list<WindowDescription> WindowDescriptionList;
+
+class WindowPicker {
+ public:
+  virtual ~WindowPicker() {}
+  virtual bool Init() = 0;
+
+  // TODO: Move this two methods to window.h when we no longer need to load
+  // CoreGraphics dynamically.
+  virtual bool IsVisible(WindowId id) = 0;
+  virtual bool MoveToFront(WindowId id) = 0;
+
+  // Gets a list of window description.
+  // Returns true if successful.
+  virtual bool GetWindowList(WindowDescriptionList* descriptions) = 0;
+};
+
+}  // namespace talk_base
+
+#endif  // TALK_BASE_WINDOWPICKER_H_
diff --git a/talk/base/winfirewall_unittest.cc b/talk/base/winfirewall_unittest.cc
new file mode 100644
index 0000000..9987716
--- /dev/null
+++ b/talk/base/winfirewall_unittest.cc
@@ -0,0 +1,57 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#include "talk/base/gunit.h"
+#include "talk/base/winfirewall.h"
+
+#include <objbase.h>
+
+namespace talk_base {
+
+TEST(WinFirewallTest, ReadStatus) {
+  ::CoInitialize(NULL);
+  WinFirewall fw;
+  HRESULT hr;
+  bool authorized;
+
+  EXPECT_FALSE(fw.QueryAuthorized("bogus.exe", &authorized));
+  EXPECT_TRUE(fw.Initialize(&hr));
+  EXPECT_EQ(S_OK, hr);
+
+  EXPECT_TRUE(fw.QueryAuthorized("bogus.exe", &authorized));
+
+  // Unless we mock out INetFwMgr we can't really have an expectation either way
+  // about whether we're authorized.  It will depend on the settings of the
+  // machine running the test.  Same goes for AddApplication.
+
+  fw.Shutdown();
+  EXPECT_FALSE(fw.QueryAuthorized("bogus.exe", &authorized));
+
+  ::CoUninitialize();
+}
+
+}  // namespace talk_base
diff --git a/talk/examples/call/call_unittest.cc b/talk/examples/call/call_unittest.cc
new file mode 100644
index 0000000..d95f1dd
--- /dev/null
+++ b/talk/examples/call/call_unittest.cc
@@ -0,0 +1,37 @@
+/*
+ * libjingle
+ * Copyright 2008, 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.
+ */
+
+// Main function for all unit tests in talk/examples/call
+
+#include "talk/base/logging.h"
+#include "testing/base/public/gunit.h"
+
+int main(int argc, char **argv) {
+  talk_base::LogMessage::LogToDebug(talk_base::LogMessage::NO_LOGGING);
+  testing::ParseGUnitFlags(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/talk/examples/call/callclient.cc b/talk/examples/call/callclient.cc
index b427783..ad18fa1 100644
--- a/talk/examples/call/callclient.cc
+++ b/talk/examples/call/callclient.cc
@@ -43,7 +43,6 @@
 #include "talk/examples/call/mucinvitesendtask.h"
 #include "talk/examples/call/friendinvitesendtask.h"
 #include "talk/examples/call/muc.h"
-#include "talk/examples/call/voicemailjidrequester.h"
 #include "talk/p2p/base/sessionmanager.h"
 #include "talk/p2p/client/basicportallocator.h"
 #include "talk/p2p/client/sessionmanagertask.h"
@@ -132,7 +131,6 @@
 "                      given JID and with optional bandwidth.\n"
 "  vcall [jid] [bw]    Initiates a video call to the user[/room] with\n"
 "                      the given JID and with optional bandwidth.\n"
-"  voicemail [jid]     Leave a voicemail for the user with the given JID.\n"
 "  join [room_jid]     Joins a multi-user-chat with room JID.\n"
 "  ljoin [room_name]   Joins a MUC by looking up JID from room name.\n"
 "  invite user [room]  Invites a friend to a multi-user-chat.\n"
@@ -267,8 +265,6 @@
       GetDevices();
     } else if ((words.size() == 2) && (command == "setvol")) {
       SetVolume(words[1]);
-    } else if (command == "voicemail") {
-      CallVoicemail((words.size() >= 2) ? words[1] : "");
     } else {
       console_->PrintLine(CONSOLE_COMMANDS);
     }
@@ -621,10 +617,6 @@
     // if the first character is a +, assume it's a phone number
     found_jid = callto_jid;
     found = true;
-  } else if (callto_jid.resource() == "voicemail") {
-    // if the resource is /voicemail, allow that
-    found_jid = callto_jid;
-    found = true;
   } else {
     // otherwise, it's a friend
     for (RosterMap::iterator iter = roster_->begin();
@@ -769,31 +761,6 @@
   console_->PrintLine("Failed to remote mute.");
 }
 
-void CallClient::CallVoicemail(const std::string& name) {
-  buzz::Jid jid(name);
-  if (!jid.IsValid() || jid.node() == "") {
-    console_->PrintLine("Invalid JID. JIDs should be in the form user@domain.");
-    return;
-  }
-  buzz::VoicemailJidRequester *request =
-      new buzz::VoicemailJidRequester(xmpp_client_, jid, my_status_.jid());
-  request->SignalGotVoicemailJid.connect(this,
-                                         &CallClient::OnFoundVoicemailJid);
-  request->SignalVoicemailJidError.connect(this,
-                                           &CallClient::OnVoicemailJidError);
-  request->Start();
-}
-
-void CallClient::OnFoundVoicemailJid(const buzz::Jid& to,
-                                     const buzz::Jid& voicemail) {
-  console_->PrintLine("Calling %s's voicemail.", to.Str().c_str());
-  PlaceCall(voicemail, cricket::CallOptions());
-}
-
-void CallClient::OnVoicemailJidError(const buzz::Jid& to) {
-  console_->PrintLine("Unable to voicemail %s.", to.Str().c_str());
-}
-
 void CallClient::Accept(const cricket::CallOptions& options) {
   ASSERT(call_ && incoming_call_);
   ASSERT(call_->sessions().size() == 1);
diff --git a/talk/examples/call/callclient.h b/talk/examples/call/callclient.h
index 3ba9994..825fd7d 100644
--- a/talk/examples/call/callclient.h
+++ b/talk/examples/call/callclient.h
@@ -47,7 +47,6 @@
 class MucInviteRecvTask;
 class MucInviteSendTask;
 class FriendInviteSendTask;
-class VoicemailJidRequester;
 class DiscoInfoQueryTask;
 class Muc;
 class Status;
@@ -194,8 +193,6 @@
                                 const std::string& mutee_nick,
                                 const buzz::XmlElement* stanza);
   void OnDevicesChange();
-  void OnFoundVoicemailJid(const buzz::Jid& to, const buzz::Jid& voicemail);
-  void OnVoicemailJidError(const buzz::Jid& to);
   void OnMediaSourcesUpdate(cricket::Call* call,
                             cricket::Session* session,
                             const cricket::MediaSources& sources);
@@ -225,7 +222,6 @@
   void PrintRoster();
   void MakeCallTo(const std::string& name, const cricket::CallOptions& options);
   void PlaceCall(const buzz::Jid& jid, const cricket::CallOptions& options);
-  void CallVoicemail(const std::string& name);
   void Accept(const cricket::CallOptions& options);
   void Reject();
   void Quit();
diff --git a/talk/examples/call/callclient_unittest.cc b/talk/examples/call/callclient_unittest.cc
new file mode 100644
index 0000000..e16ad95
--- /dev/null
+++ b/talk/examples/call/callclient_unittest.cc
@@ -0,0 +1,47 @@
+/*
+ * libjingle
+ * Copyright 2008, 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.
+ */
+
+// Unit tests for CallClient
+
+#include "talk/base/gunit.h"
+#include "talk/examples/call/callclient.h"
+#include "talk/examples/login/xmppthread.h"
+#include "talk/session/phone/mediaengine.h"
+#include "talk/session/phone/filemediaengine.h"
+
+TEST(CallClientTest, CreateCallClientWithDefaultMediaEngine) {
+  XmppPump pump;
+  CallClient *client = new CallClient(pump.client());
+  delete client;
+}
+
+TEST(CallClientTest, CreateCallClientWithFileMediaEngine) {
+  XmppPump pump;
+  CallClient *client = new CallClient(pump.client());
+  client->SetMediaEngine(new cricket::FileMediaEngine);
+  delete client;
+}
diff --git a/talk/examples/login/autoportallocator.h b/talk/examples/login/autoportallocator.h
new file mode 100644
index 0000000..f1993ca
--- /dev/null
+++ b/talk/examples/login/autoportallocator.h
@@ -0,0 +1,70 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#ifndef TALK_EXAMPLES_LOGIN_AUTOPORTALLOCATOR_H_
+#define TALK_EXAMPLES_LOGIN_AUTOPORTALLOCATOR_H_
+
+#include <string>
+#include <vector>
+
+#include "talk/examples/login/jingleinfotask.h"
+#include "talk/base/sigslot.h"
+#include "talk/p2p/client/httpportallocator.h"
+#include "talk/xmpp/xmppclient.h"
+
+// This class sets the relay and stun servers using XmppClient.
+// It enables the client to traverse Proxy and NAT.
+class AutoPortAllocator : public cricket::HttpPortAllocator,
+    public sigslot::has_slots<> {
+ public:
+  AutoPortAllocator(talk_base::NetworkManager* network_manager,
+                    const std::string& user_agent)
+      : cricket::HttpPortAllocator(network_manager, user_agent) {
+  }
+
+  // Creates and initiates a task to get relay token from XmppClient and set
+  // it appropriately.
+  void SetXmppClient(buzz::XmppClient* client) {
+    // The JingleInfoTask is freed by the task-runner.
+    buzz::JingleInfoTask* jit = new buzz::JingleInfoTask(client);
+    jit->SignalJingleInfo.connect(this, &AutoPortAllocator::OnJingleInfo);
+    jit->Start();
+    jit->RefreshJingleInfoNow();
+  }
+
+ private:
+  void OnJingleInfo(
+      const std::string& token,
+      const std::vector<std::string>& relay_hosts,
+      const std::vector<talk_base::SocketAddress>& stun_hosts) {
+    SetRelayToken(token);
+    SetStunHosts(stun_hosts);
+    SetRelayHosts(relay_hosts);
+  }
+};
+
+#endif  // TALK_EXAMPLES_LOGIN_AUTOPORTALLOCATOR_H_
diff --git a/talk/examples/login/jingleinfotask.cc b/talk/examples/login/jingleinfotask.cc
new file mode 100644
index 0000000..c8932cd
--- /dev/null
+++ b/talk/examples/login/jingleinfotask.cc
@@ -0,0 +1,137 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#include "talk/examples/login/jingleinfotask.h"
+#include "talk/base/socketaddress.h"
+#include "talk/xmpp/constants.h"
+#include "talk/xmpp/xmppclient.h"
+#include "talk/xmpp/xmpptask.h"
+
+namespace buzz {
+
+class JingleInfoTask::JingleInfoGetTask : public XmppTask {
+ public:
+  explicit JingleInfoGetTask(XmppTaskParentInterface* parent)
+      : XmppTask(parent, XmppEngine::HL_SINGLE),
+        done_(false) {}
+
+  virtual int ProcessStart() {
+    talk_base::scoped_ptr<XmlElement> get(
+        MakeIq(STR_GET, Jid(), task_id()));
+    get->AddElement(new XmlElement(QN_JINGLE_INFO_QUERY, true));
+    if (SendStanza(get.get()) != XMPP_RETURN_OK) {
+      return STATE_ERROR;
+    }
+    return STATE_RESPONSE;
+  }
+  virtual int ProcessResponse() {
+    if (done_)
+      return STATE_DONE;
+    return STATE_BLOCKED;
+  }
+
+ protected:
+  virtual bool HandleStanza(const XmlElement * stanza) {
+    if (!MatchResponseIq(stanza, Jid(), task_id()))
+      return false;
+
+    if (stanza->Attr(QN_TYPE) != STR_RESULT)
+      return false;
+
+    // Queue the stanza with the parent so these don't get handled out of order
+    JingleInfoTask* parent = static_cast<JingleInfoTask*>(GetParent());
+    parent->QueueStanza(stanza);
+
+    // Wake ourselves so we can go into the done state
+    done_ = true;
+    Wake();
+    return true;
+  }
+
+  bool done_;
+};
+
+
+void JingleInfoTask::RefreshJingleInfoNow() {
+  JingleInfoGetTask* get_task = new JingleInfoGetTask(this);
+  get_task->Start();
+}
+
+bool
+JingleInfoTask::HandleStanza(const XmlElement * stanza) {
+  if (!MatchRequestIq(stanza, "set", QN_JINGLE_INFO_QUERY))
+    return false;
+
+  // only respect relay push from the server
+  Jid from(stanza->Attr(QN_FROM));
+  if (!from.IsEmpty() &&
+      !from.BareEquals(GetClient()->jid()) &&
+      from != Jid(GetClient()->jid().domain()))
+    return false;
+
+  QueueStanza(stanza);
+  return true;
+}
+
+int
+JingleInfoTask::ProcessStart() {
+  std::vector<std::string> relay_hosts;
+  std::vector<talk_base::SocketAddress> stun_hosts;
+  std::string relay_token;
+  const XmlElement * stanza = NextStanza();
+  if (stanza == NULL)
+    return STATE_BLOCKED;
+  const XmlElement * query = stanza->FirstNamed(QN_JINGLE_INFO_QUERY);
+  if (query == NULL)
+    return STATE_START;
+  const XmlElement *stun = query->FirstNamed(QN_JINGLE_INFO_STUN);
+  if (stun) {
+    for (const XmlElement *server = stun->FirstNamed(QN_JINGLE_INFO_SERVER);
+         server != NULL; server = server->NextNamed(QN_JINGLE_INFO_SERVER)) {
+      std::string host = server->Attr(QN_JINGLE_INFO_HOST);
+      std::string port = server->Attr(QN_JINGLE_INFO_UDP);
+      if (host != STR_EMPTY && host != STR_EMPTY) {
+        stun_hosts.push_back(talk_base::SocketAddress(host, atoi(port.c_str())));
+      }
+    }
+  }
+
+  const XmlElement *relay = query->FirstNamed(QN_JINGLE_INFO_RELAY);
+  if (relay) {
+    relay_token = relay->TextNamed(QN_JINGLE_INFO_TOKEN);
+    for (const XmlElement *server = relay->FirstNamed(QN_JINGLE_INFO_SERVER);
+         server != NULL; server = server->NextNamed(QN_JINGLE_INFO_SERVER)) {
+      std::string host = server->Attr(QN_JINGLE_INFO_HOST);
+      if (host != STR_EMPTY) {
+        relay_hosts.push_back(host);
+      }
+    }
+  }
+  SignalJingleInfo(relay_token, relay_hosts, stun_hosts);
+  return STATE_START;
+}
+}
diff --git a/talk/examples/login/jingleinfotask.h b/talk/examples/login/jingleinfotask.h
new file mode 100644
index 0000000..dbc3fb0
--- /dev/null
+++ b/talk/examples/login/jingleinfotask.h
@@ -0,0 +1,61 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#ifndef TALK_EXAMPLES_LOGIN_JINGLEINFOTASK_H_
+#define TALK_EXAMPLES_LOGIN_JINGLEINFOTASK_H_
+
+#include <vector>
+
+#include "talk/p2p/client/httpportallocator.h"
+#include "talk/xmpp/xmppengine.h"
+#include "talk/xmpp/xmpptask.h"
+#include "talk/base/sigslot.h"
+
+namespace buzz {
+
+class JingleInfoTask : public XmppTask {
+ public:
+  explicit JingleInfoTask(XmppTaskParentInterface* parent) :
+    XmppTask(parent, XmppEngine::HL_TYPE) {}
+
+  virtual int ProcessStart();
+  void RefreshJingleInfoNow();
+
+  sigslot::signal3<const std::string &,
+                   const std::vector<std::string> &,
+                   const std::vector<talk_base::SocketAddress> &>
+                       SignalJingleInfo;
+
+ protected:
+  class JingleInfoGetTask;
+  friend class JingleInfoGetTask;
+
+  virtual bool HandleStanza(const XmlElement * stanza);
+};
+}
+
+#endif  // TALK_EXAMPLES_LOGIN_JINGLEINFOTASK_H_
diff --git a/talk/examples/pcp/pcp_main.cc b/talk/examples/pcp/pcp_main.cc
new file mode 100644
index 0000000..3ec263f
--- /dev/null
+++ b/talk/examples/pcp/pcp_main.cc
@@ -0,0 +1,709 @@
+#define _CRT_SECURE_NO_DEPRECATE 1
+
+#include <time.h>
+
+#include <iomanip>
+#include <iostream>
+#include <string>
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif  // HAVE_CONFIG_H
+
+#if HAVE_OPENSSL_SSL_H
+#define USE_SSL_TUNNEL
+#endif
+
+#include "talk/base/basicdefs.h"
+#include "talk/base/common.h"
+#include "talk/base/helpers.h"
+#include "talk/base/logging.h"
+#include "talk/base/ssladapter.h"
+#include "talk/base/stringutils.h"
+#include "talk/base/thread.h"
+#include "talk/examples/login/autoportallocator.h"
+#include "talk/examples/login/xmpppump.h"
+#include "talk/examples/login/xmppsocket.h"
+#include "talk/p2p/base/sessionmanager.h"
+#include "talk/p2p/client/sessionmanagertask.h"
+#include "talk/xmpp/xmppengine.h"
+#ifdef USE_SSL_TUNNEL
+#include "talk/session/tunnel/securetunnelsessionclient.h"
+#endif
+#include "talk/session/tunnel/tunnelsessionclient.h"
+#include "talk/xmpp/xmppclient.h"
+#include "talk/xmpp/xmppclientsettings.h"
+
+#ifndef MAX_PATH
+#define MAX_PATH 256
+#endif
+
+#if defined(_MSC_VER) && (_MSC_VER < 1400)
+// The following are necessary to properly link when compiling STL without
+// /EHsc, otherwise known as C++ exceptions.
+void __cdecl std::_Throw(const std::exception &) {}
+std::_Prhand std::_Raise_handler = 0;
+#endif
+
+enum {
+  MSG_LOGIN_COMPLETE = 1,
+  MSG_LOGIN_FAILED,
+  MSG_DONE,
+};
+
+buzz::Jid gUserJid;
+talk_base::InsecureCryptStringImpl gUserPass;
+std::string gXmppHost = "talk.google.com";
+int gXmppPort = 5222;
+buzz::TlsOptions gXmppUseTls = buzz::TLS_REQUIRED;
+
+class DebugLog : public sigslot::has_slots<> {
+public:
+  DebugLog() :
+    debug_input_buf_(NULL), debug_input_len_(0), debug_input_alloc_(0),
+    debug_output_buf_(NULL), debug_output_len_(0), debug_output_alloc_(0),
+    censor_password_(false)
+      {}
+  char * debug_input_buf_;
+  int debug_input_len_;
+  int debug_input_alloc_;
+  char * debug_output_buf_;
+  int debug_output_len_;
+  int debug_output_alloc_;
+  bool censor_password_;
+
+  void Input(const char * data, int len) {
+    if (debug_input_len_ + len > debug_input_alloc_) {
+      char * old_buf = debug_input_buf_;
+      debug_input_alloc_ = 4096;
+      while (debug_input_alloc_ < debug_input_len_ + len) {
+        debug_input_alloc_ *= 2;
+      }
+      debug_input_buf_ = new char[debug_input_alloc_];
+      memcpy(debug_input_buf_, old_buf, debug_input_len_);
+      delete[] old_buf;
+    }
+    memcpy(debug_input_buf_ + debug_input_len_, data, len);
+    debug_input_len_ += len;
+    DebugPrint(debug_input_buf_, &debug_input_len_, false);
+  }
+
+  void Output(const char * data, int len) {
+    if (debug_output_len_ + len > debug_output_alloc_) {
+      char * old_buf = debug_output_buf_;
+      debug_output_alloc_ = 4096;
+      while (debug_output_alloc_ < debug_output_len_ + len) {
+        debug_output_alloc_ *= 2;
+      }
+      debug_output_buf_ = new char[debug_output_alloc_];
+      memcpy(debug_output_buf_, old_buf, debug_output_len_);
+      delete[] old_buf;
+    }
+    memcpy(debug_output_buf_ + debug_output_len_, data, len);
+    debug_output_len_ += len;
+    DebugPrint(debug_output_buf_, &debug_output_len_, true);
+  }
+
+  static bool
+  IsAuthTag(const char * str, size_t len) {
+    if (str[0] == '<' && str[1] == 'a' &&
+                         str[2] == 'u' &&
+                         str[3] == 't' &&
+                         str[4] == 'h' &&
+                         str[5] <= ' ') {
+      std::string tag(str, len);
+
+      if (tag.find("mechanism") != std::string::npos)
+        return true;
+
+    }
+    return false;
+  }
+
+  void
+  DebugPrint(char * buf, int * plen, bool output) {
+    int len = *plen;
+    if (len > 0) {
+      time_t tim = time(NULL);
+      struct tm * now = localtime(&tim);
+      char *time_string = asctime(now);
+      if (time_string) {
+        size_t time_len = strlen(time_string);
+        if (time_len > 0) {
+          time_string[time_len-1] = 0;    // trim off terminating \n
+        }
+      }
+      LOG(INFO) << (output ? "SEND >>>>>>>>>>>>>>>>>>>>>>>>>" : "RECV <<<<<<<<<<<<<<<<<<<<<<<<<")
+        << " : " << time_string;
+
+      bool indent;
+      int start = 0, nest = 3;
+      for (int i = 0; i < len; i += 1) {
+        if (buf[i] == '>') {
+          if ((i > 0) && (buf[i-1] == '/')) {
+            indent = false;
+          } else if ((start + 1 < len) && (buf[start + 1] == '/')) {
+            indent = false;
+            nest -= 2;
+          } else {
+            indent = true;
+          }
+
+          // Output a tag
+          LOG(INFO) << std::setw(nest) << " " << std::string(buf + start, i + 1 - start);
+
+          if (indent)
+            nest += 2;
+
+          // Note if it's a PLAIN auth tag
+          if (IsAuthTag(buf + start, i + 1 - start)) {
+            censor_password_ = true;
+          }
+
+          // incr
+          start = i + 1;
+        }
+
+        if (buf[i] == '<' && start < i) {
+          if (censor_password_) {
+            LOG(INFO) << std::setw(nest) << " " << "## TEXT REMOVED ##";
+            censor_password_ = false;
+          }
+          else {
+            LOG(INFO) << std::setw(nest) << " " << std::string(buf + start, i - start);
+          }
+          start = i;
+        }
+      }
+      len = len - start;
+      memcpy(buf, buf + start, len);
+      *plen = len;
+    }
+  }
+
+};
+
+static DebugLog debug_log_;
+
+// Prints out a usage message then exits.
+void Usage() {
+  std::cerr << "Usage:" << std::endl;
+  std::cerr << "  pcp [options] <my_jid>                             (server mode)" << std::endl;
+  std::cerr << "  pcp [options] <my_jid> <src_file> <dst_full_jid>:<dst_file> (client sending)" << std::endl;
+  std::cerr << "  pcp [options] <my_jid> <src_full_jid>:<src_file> <dst_file> (client rcv'ing)" << std::endl;
+  std::cerr << "           --verbose" << std::endl;
+  std::cerr << "           --xmpp-host=<host>" << std::endl;
+  std::cerr << "           --xmpp-port=<port>" << std::endl;
+  std::cerr << "           --xmpp-use-tls=(true|false)" << std::endl;
+  exit(1);
+}
+
+// Prints out an error message, a usage message, then exits.
+void Error(const std::string& msg) {
+  std::cerr << "error: " << msg << std::endl;
+  std::cerr << std::endl;
+  Usage();
+}
+
+void FatalError(const std::string& msg) {
+  std::cerr << "error: " << msg << std::endl;
+  std::cerr << std::endl;
+  exit(1);
+}
+
+// Determines whether the given string is an option.  If so, the name and
+// value are appended to the given strings.
+bool ParseArg(const char* arg, std::string* name, std::string* value) {
+  if (strncmp(arg, "--", 2) != 0)
+    return false;
+
+  const char* eq = strchr(arg + 2, '=');
+  if (eq) {
+    if (name)
+      name->append(arg + 2, eq);
+    if (value)
+      value->append(eq + 1, arg + strlen(arg));
+  } else {
+    if (name)
+      name->append(arg + 2, arg + strlen(arg));
+    if (value)
+      value->clear();
+  }
+
+  return true;
+}
+
+int ParseIntArg(const std::string& name, const std::string& value) {
+  char* end;
+  long val = strtol(value.c_str(), &end, 10);
+  if (*end != '\0')
+    Error(std::string("value of option ") + name + " must be an integer");
+  return static_cast<int>(val);
+}
+
+#ifdef WIN32
+#pragma warning(push)
+// disable "unreachable code" warning b/c it varies between dbg and opt
+#pragma warning(disable: 4702)
+#endif
+bool ParseBoolArg(const std::string& name, const std::string& value) {
+  if (value == "true")
+    return true;
+  else if (value == "false")
+    return false;
+  else {
+    Error(std::string("value of option ") + name + " must be true or false");
+    return false;
+  }
+}
+#ifdef WIN32
+#pragma warning(pop)
+#endif
+
+void ParseFileArg(const char* arg, buzz::Jid* jid, std::string* file) {
+  const char* sep = strchr(arg, ':');
+  if (!sep) {
+    *file = arg;
+  } else {
+    buzz::Jid jid_arg(std::string(arg, sep-arg));
+    if (jid_arg.IsBare())
+      Error("A full JID is required for the source or destination arguments.");
+    *jid = jid_arg;
+    *file = std::string(sep+1);
+  }
+}
+
+
+void SetConsoleEcho(bool on) {
+#ifdef WIN32
+  HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
+  if ((hIn == INVALID_HANDLE_VALUE) || (hIn == NULL))
+    return;
+
+  DWORD mode;
+  if (!GetConsoleMode(hIn, &mode))
+    return;
+
+  if (on) {
+    mode = mode | ENABLE_ECHO_INPUT;
+  } else {
+    mode = mode & ~ENABLE_ECHO_INPUT;
+  }
+
+  SetConsoleMode(hIn, mode);
+#else
+  int re;
+  if (on)
+    re = system("stty echo");
+  else
+    re = system("stty -echo");
+  if (-1 == re)
+    return;
+#endif
+}
+
+// Fills in a settings object with the values from the arguments.
+buzz::XmppClientSettings LoginSettings() {
+  buzz::XmppClientSettings xcs;
+  xcs.set_user(gUserJid.node());
+  xcs.set_host(gUserJid.domain());
+  xcs.set_resource("pcp");
+  xcs.set_pass(talk_base::CryptString(gUserPass));
+  talk_base::SocketAddress server(gXmppHost, gXmppPort);
+  xcs.set_server(server);
+  xcs.set_use_tls(gXmppUseTls);
+  return xcs;
+}
+
+// Runs the current thread until a message with the given ID is seen.
+uint32 Loop(const std::vector<uint32>& ids) {
+  talk_base::Message msg;
+  while (talk_base::Thread::Current()->Get(&msg)) {
+    if (msg.phandler == NULL) {
+      if (std::find(ids.begin(), ids.end(), msg.message_id) != ids.end())
+        return msg.message_id;
+      std::cout << "orphaned message: " << msg.message_id;
+      continue;
+    }
+    talk_base::Thread::Current()->Dispatch(&msg);
+  }
+  return 0;
+}
+
+#ifdef WIN32
+#pragma warning(disable:4355)
+#endif
+
+class CustomXmppPump : public XmppPumpNotify, public XmppPump {
+public:
+  CustomXmppPump() : XmppPump(this), server_(false) { }
+
+  void Serve(cricket::TunnelSessionClient* client) {
+    client->SignalIncomingTunnel.connect(this,
+      &CustomXmppPump::OnIncomingTunnel);
+    server_ = true;
+  }
+
+  void OnStateChange(buzz::XmppEngine::State state) {
+    switch (state) {
+    case buzz::XmppEngine::STATE_START:
+      std::cout << "connecting..." << std::endl;
+      break;
+    case buzz::XmppEngine::STATE_OPENING:
+      std::cout << "logging in..." << std::endl;
+      break;
+    case buzz::XmppEngine::STATE_OPEN:
+      std::cout << "logged in..." << std::endl;
+      talk_base::Thread::Current()->Post(NULL, MSG_LOGIN_COMPLETE);
+      break;
+    case buzz::XmppEngine::STATE_CLOSED:
+      std::cout << "logged out..." << std::endl;
+      talk_base::Thread::Current()->Post(NULL, MSG_LOGIN_FAILED);
+      break;
+    }
+  }
+
+  void OnIncomingTunnel(cricket::TunnelSessionClient* client, buzz::Jid jid,
+    std::string description, cricket::Session* session) {
+    std::cout << "IncomingTunnel from " << jid.Str()
+      << ": " << description << std::endl;
+    if (!server_ || (file_.get() != NULL)) {
+      client->DeclineTunnel(session);
+      return;
+    }
+    std::string filename;
+    bool send;
+    if (strncmp(description.c_str(), "send:", 5) == 0) {
+      send = true;
+    } else if (strncmp(description.c_str(), "recv:", 5) == 0) {
+      send = false;
+    } else {
+      client->DeclineTunnel(session);
+      return;
+    }
+    filename = description.substr(5);
+    talk_base::StreamInterface* stream = client->AcceptTunnel(session);
+    if (!ProcessStream(stream, filename, send))
+      talk_base::Thread::Current()->Post(NULL, MSG_DONE);
+
+    // TODO: There is a potential memory leak, however, since the PCP
+    // app doesn't work right now, I can't verify the fix actually works, so
+    // comment out the following line until we fix the PCP app.
+
+    // delete stream;
+  }
+
+  bool ProcessStream(talk_base::StreamInterface* stream,
+                     const std::string& filename, bool send) {
+    ASSERT(file_.get() == NULL);
+    sending_ = send;
+    file_.reset(new talk_base::FileStream);
+    buffer_len_ = 0;
+    int err;
+    if (!file_->Open(filename.c_str(), sending_ ? "rb" : "wb", &err)) {
+      std::cerr << "Error opening <" << filename << ">: "
+                << std::strerror(err) << std::endl;
+      return false;
+    }
+    stream->SignalEvent.connect(this, &CustomXmppPump::OnStreamEvent);
+    if (stream->GetState() == talk_base::SS_CLOSED) {
+      std::cerr << "Failed to establish P2P tunnel" << std::endl;
+      return false;
+    }
+    if (stream->GetState() == talk_base::SS_OPEN) {
+      OnStreamEvent(stream,
+        talk_base::SE_OPEN | talk_base::SE_READ | talk_base::SE_WRITE, 0);
+    }
+    return true;
+  }
+
+  void OnStreamEvent(talk_base::StreamInterface* stream, int events,
+                     int error) {
+    if (events & talk_base::SE_CLOSE) {
+      if (error == 0) {
+        std::cout << "Tunnel closed normally" << std::endl;
+      } else {
+        std::cout << "Tunnel closed with error: " << error << std::endl;
+      }
+      Cleanup(stream);
+      return;
+    }
+    if (events & talk_base::SE_OPEN) {
+      std::cout << "Tunnel connected" << std::endl;
+    }
+    talk_base::StreamResult result;
+    size_t count;
+    if (sending_ && (events & talk_base::SE_WRITE)) {
+      LOG(LS_VERBOSE) << "Tunnel SE_WRITE";
+      while (true) {
+        size_t write_pos = 0;
+        while (write_pos < buffer_len_) {
+          result = stream->Write(buffer_ + write_pos, buffer_len_ - write_pos,
+                                &count, &error);
+          if (result == talk_base::SR_SUCCESS) {
+            write_pos += count;
+            continue;
+          }
+          if (result == talk_base::SR_BLOCK) {
+            buffer_len_ -= write_pos;
+            memmove(buffer_, buffer_ + write_pos, buffer_len_);
+            LOG(LS_VERBOSE) << "Tunnel write block";
+            return;
+          }
+          if (result == talk_base::SR_EOS) {
+            std::cout << "Tunnel closed unexpectedly on write" << std::endl;
+          } else {
+            std::cout << "Tunnel write error: " << error << std::endl;
+          }
+          Cleanup(stream);
+          return;
+        }
+        buffer_len_ = 0;
+        while (buffer_len_ < sizeof(buffer_)) {
+          result = file_->Read(buffer_ + buffer_len_,
+                              sizeof(buffer_) - buffer_len_,
+                              &count, &error);
+          if (result == talk_base::SR_SUCCESS) {
+            buffer_len_ += count;
+            continue;
+          }
+          if (result == talk_base::SR_EOS) {
+            if (buffer_len_ > 0)
+              break;
+            std::cout << "End of file" << std::endl;
+            // A hack until we have friendly shutdown
+            Cleanup(stream, true);
+            return;
+          } else if (result == talk_base::SR_BLOCK) {
+            std::cout << "File blocked unexpectedly on read" << std::endl;
+          } else {
+            std::cout << "File read error: " << error << std::endl;
+          }
+          Cleanup(stream);
+          return;
+        }
+      }
+    }
+    if (!sending_ && (events & talk_base::SE_READ)) {
+      LOG(LS_VERBOSE) << "Tunnel SE_READ";
+      while (true) {
+        buffer_len_ = 0;
+        while (buffer_len_ < sizeof(buffer_)) {
+          result = stream->Read(buffer_ + buffer_len_,
+                                sizeof(buffer_) - buffer_len_,
+                                &count, &error);
+          if (result == talk_base::SR_SUCCESS) {
+            buffer_len_ += count;
+            continue;
+          }
+          if (result == talk_base::SR_BLOCK) {
+            if (buffer_len_ > 0)
+              break;
+            LOG(LS_VERBOSE) << "Tunnel read block";
+            return;
+          }
+          if (result == talk_base::SR_EOS) {
+            std::cout << "Tunnel closed unexpectedly on read" << std::endl;
+          } else {
+            std::cout << "Tunnel read error: " << error << std::endl;
+          }
+          Cleanup(stream);
+          return;
+        }
+        size_t write_pos = 0;
+        while (write_pos < buffer_len_) {
+          result = file_->Write(buffer_ + write_pos, buffer_len_ - write_pos,
+                                &count, &error);
+          if (result == talk_base::SR_SUCCESS) {
+            write_pos += count;
+            continue;
+          }
+          if (result == talk_base::SR_EOS) {
+            std::cout << "File closed unexpectedly on write" << std::endl;
+          } else if (result == talk_base::SR_BLOCK) {
+            std::cout << "File blocked unexpectedly on write" << std::endl;
+          } else {
+            std::cout << "File write error: " << error << std::endl;
+          }
+          Cleanup(stream);
+          return;
+        }
+      }
+    }
+  }
+
+  void Cleanup(talk_base::StreamInterface* stream, bool delay = false) {
+    LOG(LS_VERBOSE) << "Closing";
+    stream->Close();
+    file_.reset();
+    if (!server_) {
+      if (delay)
+        talk_base::Thread::Current()->PostDelayed(2000, NULL, MSG_DONE);
+      else
+        talk_base::Thread::Current()->Post(NULL, MSG_DONE);
+    }
+  }
+
+private:
+  bool server_, sending_;
+  talk_base::scoped_ptr<talk_base::FileStream> file_;
+  char buffer_[1024 * 64];
+  size_t buffer_len_;
+};
+
+int main(int argc, char **argv) {
+  talk_base::LogMessage::LogThreads();
+  talk_base::LogMessage::LogTimestamps();
+
+  // TODO: Default the username to the current users's name.
+
+  // Parse the arguments.
+
+  int index = 1;
+  while (index < argc) {
+    std::string name, value;
+    if (!ParseArg(argv[index], &name, &value))
+      break;
+
+    if (name == "help") {
+      Usage();
+    } else if (name == "verbose") {
+      talk_base::LogMessage::LogToDebug(talk_base::LS_VERBOSE);
+    } else if (name == "xmpp-host") {
+      gXmppHost = value;
+    } else if (name == "xmpp-port") {
+      gXmppPort = ParseIntArg(name, value);
+    } else if (name == "xmpp-use-tls") {
+      gXmppUseTls = ParseBoolArg(name, value)?
+          buzz::TLS_REQUIRED : buzz::TLS_DISABLED;
+    } else {
+      Error(std::string("unknown option: ") + name);
+    }
+
+    index += 1;
+  }
+
+  if (index >= argc)
+    Error("bad arguments");
+  gUserJid = buzz::Jid(argv[index++]);
+  if (!gUserJid.IsValid())
+    Error("bad arguments");
+
+  char path[MAX_PATH];
+#if WIN32
+  GetCurrentDirectoryA(MAX_PATH, path);
+#else
+  if (NULL == getcwd(path, MAX_PATH))
+    Error("Unable to get current path");
+#endif
+
+  std::cout << "Directory: " << std::string(path) << std::endl;
+
+  buzz::Jid gSrcJid;
+  buzz::Jid gDstJid;
+  std::string gSrcFile;
+  std::string gDstFile;
+
+  bool as_server = true;
+  if (index + 2 == argc) {
+    ParseFileArg(argv[index], &gSrcJid, &gSrcFile);
+    ParseFileArg(argv[index+1], &gDstJid, &gDstFile);
+    if(gSrcJid.Str().empty() == gDstJid.Str().empty())
+      Error("Exactly one of source JID or destination JID must be empty.");
+    as_server = false;
+  } else if (index != argc) {
+    Error("bad arguments");
+  }
+
+  std::cout << "Password: ";
+  SetConsoleEcho(false);
+  std::cin >> gUserPass.password();
+  SetConsoleEcho(true);
+  std::cout << std::endl;
+
+  talk_base::InitializeSSL();
+  // Log in.
+  CustomXmppPump pump;
+  pump.client()->SignalLogInput.connect(&debug_log_, &DebugLog::Input);
+  pump.client()->SignalLogOutput.connect(&debug_log_, &DebugLog::Output);
+  pump.DoLogin(LoginSettings(), new XmppSocket(gXmppUseTls), 0);
+    //new XmppAuth());
+
+  // Wait until login succeeds.
+  std::vector<uint32> ids;
+  ids.push_back(MSG_LOGIN_COMPLETE);
+  ids.push_back(MSG_LOGIN_FAILED);
+  if (MSG_LOGIN_FAILED == Loop(ids))
+    FatalError("Failed to connect");
+
+  {
+    talk_base::scoped_ptr<buzz::XmlElement> presence(
+      new buzz::XmlElement(buzz::QN_PRESENCE));
+    presence->AddElement(new buzz::XmlElement(buzz::QN_PRIORITY));
+    presence->AddText("-1", 1);
+    pump.SendStanza(presence.get());
+  }
+
+  std::string user_jid_str = pump.client()->jid().Str();
+  std::cout << "Logged in as " << user_jid_str << std::endl;
+
+  // Prepare the random number generator.
+  talk_base::InitRandom(user_jid_str.c_str(), user_jid_str.size());
+
+  // Create the P2P session manager.
+  talk_base::BasicNetworkManager network_manager;
+  AutoPortAllocator allocator(&network_manager, "pcp_agent");
+  allocator.SetXmppClient(pump.client());
+  cricket::SessionManager session_manager(&allocator);
+#ifdef USE_SSL_TUNNEL
+  cricket::SecureTunnelSessionClient session_client(pump.client()->jid(),
+                                                    &session_manager);
+  if (!session_client.GenerateIdentity())
+    FatalError("Failed to generate SSL identity");
+#else  // !USE_SSL_TUNNEL
+  cricket::TunnelSessionClient session_client(pump.client()->jid(),
+                                              &session_manager);
+#endif  // USE_SSL_TUNNEL
+  cricket::SessionManagerTask *receiver =
+      new cricket::SessionManagerTask(pump.client(), &session_manager);
+  receiver->EnableOutgoingMessages();
+  receiver->Start();
+
+  bool success = true;
+
+  // Establish the appropriate connection.
+  if (as_server) {
+    pump.Serve(&session_client);
+  } else {
+    talk_base::StreamInterface* stream = NULL;
+    std::string filename;
+    bool sending;
+    if (gSrcJid.Str().empty()) {
+      std::string message("recv:");
+      message.append(gDstFile);
+      stream = session_client.CreateTunnel(gDstJid, message);
+      filename = gSrcFile;
+      sending = true;
+    } else {
+      std::string message("send:");
+      message.append(gSrcFile);
+      stream = session_client.CreateTunnel(gSrcJid, message);
+      filename = gDstFile;
+      sending = false;
+    }
+    success = pump.ProcessStream(stream, filename, sending);
+  }
+
+  if (success) {
+    // Wait until the copy is done.
+    ids.clear();
+    ids.push_back(MSG_DONE);
+    ids.push_back(MSG_LOGIN_FAILED);
+    Loop(ids);
+  }
+
+  // Log out.
+  pump.DoDisconnect();
+
+  return 0;
+}
diff --git a/talk/examples/peerconnection/client/conductor.cc b/talk/examples/peerconnection/client/conductor.cc
new file mode 100644
index 0000000..3557f11
--- /dev/null
+++ b/talk/examples/peerconnection/client/conductor.cc
@@ -0,0 +1,399 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include "talk/examples/peerconnection/client/conductor.h"
+
+#include <utility>
+
+#ifdef WEBRTC_RELATIVE_PATH
+#include "modules/video_capture/main/interface/video_capture_factory.h"
+#else
+#include "third_party/webrtc/files/include/video_capture_factory.h"
+#endif
+#include "talk/app/webrtc/peerconnection.h"
+#include "talk/examples/peerconnection/client/defaults.h"
+#include "talk/base/common.h"
+#include "talk/base/logging.h"
+#include "talk/p2p/client/basicportallocator.h"
+#include "talk/session/phone/videorendererfactory.h"
+
+Conductor::Conductor(PeerConnectionClient* client, MainWindow* main_wnd)
+  : peer_id_(-1),
+    client_(client),
+    main_wnd_(main_wnd) {
+  client_->RegisterObserver(this);
+  main_wnd->RegisterObserver(this);
+}
+
+Conductor::~Conductor() {
+  ASSERT(peer_connection_.get() == NULL);
+}
+
+bool Conductor::connection_active() const {
+  return peer_connection_.get() != NULL;
+}
+
+void Conductor::Close() {
+  client_->SignOut();
+  DeletePeerConnection();
+}
+
+bool Conductor::InitializePeerConnection() {
+  ASSERT(peer_connection_factory_.get() == NULL);
+  ASSERT(peer_connection_.get() == NULL);
+
+  peer_connection_factory_  = webrtc::CreatePeerConnectionFactory();
+
+  if (!peer_connection_factory_.get()) {
+    main_wnd_->MessageBox("Error",
+        "Failed to initialize PeerConnectionFactory", true);
+    DeletePeerConnection();
+    return false;
+  }
+
+  peer_connection_ = peer_connection_factory_->CreatePeerConnection(
+      GetPeerConnectionString(), this);
+
+  if (!peer_connection_.get()) {
+    main_wnd_->MessageBox("Error",
+        "CreatePeerConnection failed", true);
+    DeletePeerConnection();
+  }
+  return peer_connection_.get() != NULL;
+}
+
+void Conductor::DeletePeerConnection() {
+  peer_connection_ = NULL;
+  active_streams_.clear();
+  peer_connection_factory_ = NULL;
+  peer_id_ = -1;
+}
+
+void Conductor::EnsureStreamingUI() {
+  ASSERT(peer_connection_.get() != NULL);
+  if (main_wnd_->IsWindow()) {
+    if (main_wnd_->current_ui() != MainWindow::STREAMING)
+      main_wnd_->SwitchToStreamingUI();
+  }
+}
+
+//
+// PeerConnectionObserver implementation.
+//
+
+void Conductor::OnError() {
+  LOG(LS_ERROR) << __FUNCTION__;
+  main_wnd_->QueueUIThreadCallback(PEER_CONNECTION_ERROR, NULL);
+}
+
+void Conductor::OnSignalingMessage(const std::string& msg) {
+  LOG(INFO) << __FUNCTION__;
+
+  std::string* msg_copy = new std::string(msg);
+  main_wnd_->QueueUIThreadCallback(SEND_MESSAGE_TO_PEER, msg_copy);
+}
+
+// Called when a remote stream is added
+void Conductor::OnAddStream(webrtc::MediaStreamInterface* stream) {
+  LOG(INFO) << __FUNCTION__ << " " << stream->label();
+
+  stream->AddRef();
+  main_wnd_->QueueUIThreadCallback(NEW_STREAM_ADDED,
+                                   stream);
+}
+
+void Conductor::OnRemoveStream(webrtc::MediaStreamInterface* stream) {
+  LOG(INFO) << __FUNCTION__ << " " << stream->label();
+  stream->AddRef();
+  main_wnd_->QueueUIThreadCallback(STREAM_REMOVED,
+                                   stream);
+}
+
+//
+// PeerConnectionClientObserver implementation.
+//
+
+void Conductor::OnSignedIn() {
+  LOG(INFO) << __FUNCTION__;
+  main_wnd_->SwitchToPeerList(client_->peers());
+}
+
+void Conductor::OnDisconnected() {
+  LOG(INFO) << __FUNCTION__;
+
+  DeletePeerConnection();
+
+  if (main_wnd_->IsWindow())
+    main_wnd_->SwitchToConnectUI();
+}
+
+void Conductor::OnPeerConnected(int id, const std::string& name) {
+  LOG(INFO) << __FUNCTION__;
+  // Refresh the list if we're showing it.
+  if (main_wnd_->current_ui() == MainWindow::LIST_PEERS)
+    main_wnd_->SwitchToPeerList(client_->peers());
+}
+
+void Conductor::OnPeerDisconnected(int id) {
+  LOG(INFO) << __FUNCTION__;
+  if (id == peer_id_) {
+    LOG(INFO) << "Our peer disconnected";
+    main_wnd_->QueueUIThreadCallback(PEER_CONNECTION_CLOSED, NULL);
+  } else {
+    // Refresh the list if we're showing it.
+    if (main_wnd_->current_ui() == MainWindow::LIST_PEERS)
+      main_wnd_->SwitchToPeerList(client_->peers());
+  }
+}
+
+void Conductor::OnMessageFromPeer(int peer_id, const std::string& message) {
+  ASSERT(peer_id_ == peer_id || peer_id_ == -1);
+  ASSERT(!message.empty());
+
+  if (!peer_connection_.get()) {
+    ASSERT(peer_id_ == -1);
+    peer_id_ = peer_id;
+
+    // Got an offer.  Give it to the PeerConnection instance.
+    // Once processed, we will get a callback to OnSignalingMessage with
+    // our 'answer' which we'll send to the peer.
+    LOG(INFO) << "Got an offer from our peer: " << peer_id;
+    if (!InitializePeerConnection()) {
+      LOG(LS_ERROR) << "Failed to initialize our PeerConnection instance";
+      client_->SignOut();
+      return;
+    }
+  } else if (peer_id != peer_id_) {
+    ASSERT(peer_id_ != -1);
+    LOG(WARNING) << "Received an offer from a peer while already in a "
+                    "conversation with a different peer.";
+    return;
+  }
+
+  peer_connection_->ProcessSignalingMessage(message);
+}
+
+void Conductor::OnMessageSent(int err) {
+  // Process the next pending message if any.
+  main_wnd_->QueueUIThreadCallback(SEND_MESSAGE_TO_PEER, NULL);
+}
+
+//
+// MainWndCallback implementation.
+//
+
+bool Conductor::StartLogin(const std::string& server, int port) {
+  if (client_->is_connected())
+    return false;
+
+  if (!client_->Connect(server, port, GetPeerName())) {
+    main_wnd_->MessageBox("Error", ("Failed to connect to " + server).c_str(),
+                          true);
+    return false;
+  }
+
+  return true;
+}
+
+void Conductor::DisconnectFromServer() {
+  if (client_->is_connected())
+    client_->SignOut();
+}
+
+void Conductor::ConnectToPeer(int peer_id) {
+  ASSERT(peer_id_ == -1);
+  ASSERT(peer_id != -1);
+
+  if (peer_connection_.get()) {
+    main_wnd_->MessageBox("Error",
+        "We only support connecting to one peer at a time", true);
+    return;
+  }
+
+  if (InitializePeerConnection()) {
+    peer_id_ = peer_id;
+    AddStreams();
+  } else {
+    main_wnd_->MessageBox("Error", "Failed to initialize PeerConnection", true);
+  }
+}
+
+talk_base::scoped_refptr<webrtc::VideoCaptureModule>
+Conductor::OpenVideoCaptureDevice() {
+  webrtc::VideoCaptureModule::DeviceInfo* device_info(
+      webrtc::VideoCaptureFactory::CreateDeviceInfo(0));
+  talk_base::scoped_refptr<webrtc::VideoCaptureModule> video_device;
+
+  const size_t kMaxDeviceNameLength = 128;
+  const size_t kMaxUniqueIdLength = 256;
+  uint8 device_name[kMaxDeviceNameLength];
+  uint8 unique_id[kMaxUniqueIdLength];
+
+  const size_t device_count = device_info->NumberOfDevices();
+  for (size_t i = 0; i < device_count; ++i) {
+    // Get the name of the video capture device.
+    device_info->GetDeviceName(i, device_name, kMaxDeviceNameLength, unique_id,
+        kMaxUniqueIdLength);
+    // Try to open this device.
+    video_device =
+        webrtc::VideoCaptureFactory::Create(0, unique_id);
+    if (video_device.get())
+      break;
+  }
+  delete device_info;
+  return video_device;
+}
+
+void Conductor::AddStreams() {
+  if (active_streams_.find(kStreamLabel) != active_streams_.end())
+    return;  // Already added.
+
+  talk_base::scoped_refptr<webrtc::LocalAudioTrackInterface> audio_track(
+      peer_connection_factory_->CreateLocalAudioTrack(kAudioLabel, NULL));
+
+  talk_base::scoped_refptr<webrtc::LocalVideoTrackInterface> video_track(
+      peer_connection_factory_->CreateLocalVideoTrack(
+          kVideoLabel, CreateVideoCapturer(OpenVideoCaptureDevice())));
+
+  video_track->SetRenderer(main_wnd_->local_renderer());
+
+  talk_base::scoped_refptr<webrtc::LocalMediaStreamInterface> stream =
+      peer_connection_factory_->CreateLocalMediaStream(kStreamLabel);
+
+  stream->AddTrack(audio_track);
+  stream->AddTrack(video_track);
+  peer_connection_->AddStream(stream);
+  peer_connection_->CommitStreamChanges();
+  typedef std::pair<std::string,
+                    talk_base::scoped_refptr<webrtc::MediaStreamInterface> >
+      MediaStreamPair;
+  active_streams_.insert(MediaStreamPair(stream->label(), stream));
+  main_wnd_->SwitchToStreamingUI();
+}
+
+void Conductor::DisconnectFromCurrentPeer() {
+  LOG(INFO) << __FUNCTION__;
+  if (peer_connection_.get()) {
+    client_->SendHangUp(peer_id_);
+    DeletePeerConnection();
+  }
+
+  if (main_wnd_->IsWindow())
+    main_wnd_->SwitchToPeerList(client_->peers());
+}
+
+void Conductor::UIThreadCallback(int msg_id, void* data) {
+  switch (msg_id) {
+    case PEER_CONNECTION_CLOSED:
+      LOG(INFO) << "PEER_CONNECTION_CLOSED";
+      DeletePeerConnection();
+
+      ASSERT(active_streams_.empty());
+
+      if (main_wnd_->IsWindow()) {
+        if (client_->is_connected()) {
+          main_wnd_->SwitchToPeerList(client_->peers());
+        } else {
+          main_wnd_->SwitchToConnectUI();
+        }
+      } else {
+        DisconnectFromServer();
+      }
+      break;
+
+    case SEND_MESSAGE_TO_PEER: {
+      LOG(INFO) << "SEND_MESSAGE_TO_PEER";
+      std::string* msg = reinterpret_cast<std::string*>(data);
+      if (msg) {
+        // For convenience, we always run the message through the queue.
+        // This way we can be sure that messages are sent to the server
+        // in the same order they were signaled without much hassle.
+        pending_messages_.push_back(msg);
+      }
+
+      if (!pending_messages_.empty() && !client_->IsSendingMessage()) {
+        msg = pending_messages_.front();
+        pending_messages_.pop_front();
+
+        if (!client_->SendToPeer(peer_id_, *msg) && peer_id_ != -1) {
+          LOG(LS_ERROR) << "SendToPeer failed";
+          DisconnectFromServer();
+        }
+        delete msg;
+      }
+
+      if (!peer_connection_.get())
+        peer_id_ = -1;
+
+      break;
+    }
+
+    case PEER_CONNECTION_ADDSTREAMS:
+      AddStreams();
+      break;
+
+    case PEER_CONNECTION_ERROR:
+      main_wnd_->MessageBox("Error", "an unknown error occurred", true);
+      break;
+
+    case NEW_STREAM_ADDED: {
+      webrtc::MediaStreamInterface* stream =
+          reinterpret_cast<webrtc::MediaStreamInterface*>(
+          data);
+      talk_base::scoped_refptr<webrtc::VideoTracks> tracks =
+          stream->video_tracks();
+      for (size_t i = 0; i < tracks->count(); ++i) {
+        webrtc::VideoTrackInterface* track = tracks->at(i);
+        LOG(INFO) << "Setting video renderer for track: " << track->label();
+        track->SetRenderer(main_wnd_->remote_renderer());
+      }
+      // If we haven't shared any streams with this peer (we're the receiver)
+      // then do so now.
+      if (active_streams_.empty())
+        AddStreams();
+      stream->Release();
+      break;
+    }
+
+    case STREAM_REMOVED: {
+      webrtc::MediaStreamInterface* stream =
+          reinterpret_cast<webrtc::MediaStreamInterface*>(
+          data);
+      active_streams_.erase(stream->label());
+      stream->Release();
+      if (active_streams_.empty()) {
+        LOG(INFO) << "All streams have been closed.";
+        main_wnd_->QueueUIThreadCallback(PEER_CONNECTION_CLOSED, NULL);
+      }
+      break;
+    }
+
+    default:
+      ASSERT(false);
+      break;
+  }
+}
diff --git a/talk/examples/peerconnection/client/conductor.h b/talk/examples/peerconnection/client/conductor.h
new file mode 100644
index 0000000..4268d74
--- /dev/null
+++ b/talk/examples/peerconnection/client/conductor.h
@@ -0,0 +1,134 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef PEERCONNECTION_SAMPLES_CLIENT_CONDUCTOR_H_
+#define PEERCONNECTION_SAMPLES_CLIENT_CONDUCTOR_H_
+#pragma once
+
+#include <deque>
+#include <map>
+#include <set>
+#include <string>
+
+#include "talk/examples/peerconnection/client/main_wnd.h"
+#include "talk/examples/peerconnection/client/peer_connection_client.h"
+#include "talk/app/webrtc/mediastream.h"
+#include "talk/app/webrtc/peerconnection.h"
+#include "talk/base/scoped_ptr.h"
+
+namespace webrtc {
+class VideoCaptureModule;
+}  // namespace webrtc
+
+namespace cricket {
+class VideoRenderer;
+}  // namespace cricket
+
+class Conductor
+  : public webrtc::PeerConnectionObserver,
+    public PeerConnectionClientObserver,
+    public MainWndCallback {
+ public:
+  enum CallbackID {
+    MEDIA_CHANNELS_INITIALIZED = 1,
+    PEER_CONNECTION_CLOSED,
+    SEND_MESSAGE_TO_PEER,
+    PEER_CONNECTION_ADDSTREAMS,
+    PEER_CONNECTION_ERROR,
+    NEW_STREAM_ADDED,
+    STREAM_REMOVED,
+  };
+
+  Conductor(PeerConnectionClient* client, MainWindow* main_wnd);
+  ~Conductor();
+
+  bool connection_active() const;
+
+  virtual void Close();
+
+ protected:
+  bool InitializePeerConnection();
+  void DeletePeerConnection();
+  void EnsureStreamingUI();
+  void AddStreams();
+  talk_base::scoped_refptr<webrtc::VideoCaptureModule> OpenVideoCaptureDevice();
+
+  //
+  // PeerConnectionObserver implementation.
+  //
+  virtual void OnError();
+  virtual void OnMessage(const std::string& msg) {}
+  virtual void OnSignalingMessage(const std::string& msg);
+  virtual void OnStateChange(
+      webrtc::PeerConnectionObserver::StateType state_changed) {}
+  virtual void OnAddStream(webrtc::MediaStreamInterface* stream);
+  virtual void OnRemoveStream(webrtc::MediaStreamInterface* stream);
+
+
+  //
+  // PeerConnectionClientObserver implementation.
+  //
+
+  virtual void OnSignedIn();
+
+  virtual void OnDisconnected();
+
+  virtual void OnPeerConnected(int id, const std::string& name);
+
+  virtual void OnPeerDisconnected(int id);
+
+  virtual void OnMessageFromPeer(int peer_id, const std::string& message);
+
+  virtual void OnMessageSent(int err);
+
+  //
+  // MainWndCallback implementation.
+  //
+
+  virtual bool StartLogin(const std::string& server, int port);
+
+  virtual void DisconnectFromServer();
+
+  virtual void ConnectToPeer(int peer_id);
+
+  virtual void DisconnectFromCurrentPeer();
+
+  virtual void UIThreadCallback(int msg_id, void* data);
+
+ protected:
+  int peer_id_;
+  talk_base::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection_;
+  talk_base::scoped_refptr<webrtc::PeerConnectionFactoryInterface>
+      peer_connection_factory_;
+  PeerConnectionClient* client_;
+  MainWindow* main_wnd_;
+  std::deque<std::string*> pending_messages_;
+  std::map<std::string, talk_base::scoped_refptr<webrtc::MediaStreamInterface> >
+      active_streams_;
+};
+
+#endif  // PEERCONNECTION_SAMPLES_CLIENT_CONDUCTOR_H_
diff --git a/talk/examples/peerconnection/client/defaults.cc b/talk/examples/peerconnection/client/defaults.cc
new file mode 100644
index 0000000..eb895d0
--- /dev/null
+++ b/talk/examples/peerconnection/client/defaults.cc
@@ -0,0 +1,75 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include "talk/examples/peerconnection/client/defaults.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef WIN32
+#include <winsock2.h>
+#else
+#include <unistd.h>
+#endif
+
+#include "talk/base/common.h"
+
+const char kAudioLabel[] = "audio_label";
+const char kVideoLabel[] = "video_label";
+const char kStreamLabel[] = "stream_label";
+const uint16 kDefaultServerPort = 8888;
+
+std::string GetEnvVarOrDefault(const char* env_var_name,
+                               const char* default_value) {
+  std::string value;
+  const char* env_var = getenv(env_var_name);
+  if (env_var)
+    value = env_var;
+
+  if (value.empty())
+    value = default_value;
+
+  return value;
+}
+
+std::string GetPeerConnectionString() {
+  return GetEnvVarOrDefault("WEBRTC_CONNECT", "STUN stun.l.google.com:19302");
+}
+
+std::string GetDefaultServerName() {
+  return GetEnvVarOrDefault("WEBRTC_SERVER", "localhost");
+}
+
+std::string GetPeerName() {
+  char computer_name[256];
+  if (gethostname(computer_name, ARRAY_SIZE(computer_name)) != 0)
+    strcpy(computer_name, "host");
+  std::string ret(GetEnvVarOrDefault("USERNAME", "user"));
+  ret += '@';
+  ret += computer_name;
+  return ret;
+}
diff --git a/talk/examples/peerconnection/client/defaults.h b/talk/examples/peerconnection/client/defaults.h
new file mode 100644
index 0000000..f646149
--- /dev/null
+++ b/talk/examples/peerconnection/client/defaults.h
@@ -0,0 +1,47 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef PEERCONNECTION_SAMPLES_CLIENT_DEFAULTS_H_
+#define PEERCONNECTION_SAMPLES_CLIENT_DEFAULTS_H_
+#pragma once
+
+#include <string>
+
+#include "talk/base/basictypes.h"
+
+extern const char kAudioLabel[];
+extern const char kVideoLabel[];
+extern const char kStreamLabel[];
+extern const uint16 kDefaultServerPort;
+
+std::string GetEnvVarOrDefault(const char* env_var_name,
+                               const char* default_value);
+std::string GetPeerConnectionString();
+std::string GetDefaultServerName();
+std::string GetPeerName();
+
+#endif  // PEERCONNECTION_SAMPLES_CLIENT_DEFAULTS_H_
diff --git a/talk/examples/peerconnection/client/linux/main.cc b/talk/examples/peerconnection/client/linux/main.cc
new file mode 100644
index 0000000..ab912ab
--- /dev/null
+++ b/talk/examples/peerconnection/client/linux/main.cc
@@ -0,0 +1,102 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include <gtk/gtk.h>
+
+#include "talk/examples/peerconnection/client/conductor.h"
+#include "talk/examples/peerconnection/client/linux/main_wnd.h"
+#include "talk/examples/peerconnection/client/peer_connection_client.h"
+
+#include "talk/base/thread.h"
+
+class CustomSocketServer : public talk_base::PhysicalSocketServer {
+ public:
+  CustomSocketServer(talk_base::Thread* thread, GtkMainWnd* wnd)
+      : thread_(thread), wnd_(wnd), conductor_(NULL), client_(NULL) {}
+  virtual ~CustomSocketServer() {}
+  
+  void set_client(PeerConnectionClient* client) { client_ = client; }
+  void set_conductor(Conductor* conductor) { conductor_ = conductor; }
+
+  // Override so that we can also pump the GTK message loop.
+  virtual bool Wait(int cms, bool process_io) {
+    // Pump GTK events.
+    // TODO: We really should move either the socket server or UI to a
+    // different thread.  Alternatively we could look at merging the two loops
+    // by implementing a dispatcher for the socket server and/or use
+    // g_main_context_set_poll_func.
+      while (gtk_events_pending())
+        gtk_main_iteration();
+    
+    if (!wnd_->IsWindow() && !conductor_->connection_active() &&
+        client_ != NULL && !client_->is_connected()) {
+      thread_->Quit();
+    }
+    return talk_base::PhysicalSocketServer::Wait(0/*cms == -1 ? 1 : cms*/,
+                                                 process_io);
+  }
+
+ protected:
+  talk_base::Thread* thread_;
+  GtkMainWnd* wnd_;
+  Conductor* conductor_;
+  PeerConnectionClient* client_;
+};
+
+int main(int argc, char* argv[]) {
+  gtk_init(&argc, &argv);
+  g_type_init();
+  g_thread_init(NULL);
+
+  GtkMainWnd wnd;
+  wnd.Create();
+
+  talk_base::AutoThread auto_thread;
+  talk_base::Thread* thread = talk_base::Thread::Current();
+  CustomSocketServer socket_server(thread, &wnd);
+  thread->set_socketserver(&socket_server);
+
+  // Must be constructed after we set the socketserver.
+  PeerConnectionClient client;
+  Conductor conductor(&client, &wnd);
+  socket_server.set_client(&client);
+  socket_server.set_conductor(&conductor);
+
+  thread->Run();
+
+  // gtk_main();
+  wnd.Destroy();
+
+  thread->set_socketserver(NULL);
+  // TODO: Run the Gtk main loop to tear down the connection.
+  //while (gtk_events_pending()) {
+  //  gtk_main_iteration();
+  //}
+
+  return 0;
+}
+
diff --git a/talk/examples/peerconnection/client/linux/main_wnd.cc b/talk/examples/peerconnection/client/linux/main_wnd.cc
new file mode 100644
index 0000000..b417544
--- /dev/null
+++ b/talk/examples/peerconnection/client/linux/main_wnd.cc
@@ -0,0 +1,481 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+
+#include "talk/examples/peerconnection/client/linux/main_wnd.h"
+
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+#include <stddef.h>
+
+#include "talk/examples/peerconnection/client/defaults.h"
+#include "talk/base/common.h"
+#include "talk/base/logging.h"
+#include "talk/base/stringutils.h"
+
+using talk_base::sprintfn;
+
+namespace {
+
+//
+// Simple static functions that simply forward the callback to the
+// GtkMainWnd instance.
+//
+
+gboolean OnDestroyedCallback(GtkWidget* widget, GdkEvent* event,
+                             gpointer data) {
+  reinterpret_cast<GtkMainWnd*>(data)->OnDestroyed(widget, event);
+  return FALSE;
+}
+
+void OnClickedCallback(GtkWidget* widget, gpointer data) {
+  reinterpret_cast<GtkMainWnd*>(data)->OnClicked(widget);
+}
+
+gboolean OnKeyPressCallback(GtkWidget* widget, GdkEventKey* key,
+                            gpointer data) {
+  reinterpret_cast<GtkMainWnd*>(data)->OnKeyPress(widget, key);
+  return false;
+}
+
+void OnRowActivatedCallback(GtkTreeView* tree_view, GtkTreePath* path,
+                            GtkTreeViewColumn* column, gpointer data) {
+  reinterpret_cast<GtkMainWnd*>(data)->OnRowActivated(tree_view, path, column);
+}
+
+// Creates a tree view, that we use to display the list of peers.
+void InitializeList(GtkWidget* list) {
+  GtkCellRenderer* renderer = gtk_cell_renderer_text_new();
+  GtkTreeViewColumn* column = gtk_tree_view_column_new_with_attributes(
+      "List Items", renderer, "text", 0, NULL);
+  gtk_tree_view_append_column(GTK_TREE_VIEW(list), column);
+  GtkListStore* store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
+  gtk_tree_view_set_model(GTK_TREE_VIEW(list), GTK_TREE_MODEL(store));
+  g_object_unref(store);
+}
+
+// Adds an entry to a tree view.
+void AddToList(GtkWidget* list, const gchar* str, int value) {
+  GtkListStore* store = GTK_LIST_STORE(
+      gtk_tree_view_get_model(GTK_TREE_VIEW(list)));
+
+  GtkTreeIter iter;
+  gtk_list_store_append(store, &iter);
+  gtk_list_store_set(store, &iter, 0, str, 1, value, -1);
+}
+
+struct UIThreadCallbackData {
+  explicit UIThreadCallbackData(MainWndCallback* cb, int id, void* d)
+      : callback(cb), msg_id(id), data(d) {}
+  MainWndCallback* callback;
+  int msg_id;
+  void* data;
+};
+
+gboolean HandleUIThreadCallback(gpointer data) {
+  UIThreadCallbackData* cb_data = reinterpret_cast<UIThreadCallbackData*>(data);
+  cb_data->callback->UIThreadCallback(cb_data->msg_id, cb_data->data);
+  delete cb_data;
+  return false;
+}
+
+gboolean Redraw(gpointer data) {
+  GtkMainWnd* wnd = reinterpret_cast<GtkMainWnd*>(data);
+  wnd->OnRedraw();
+  return false;
+}
+}  // end anonymous
+
+//
+// GtkMainWnd implementation.
+//
+
+GtkMainWnd::GtkMainWnd()
+    : window_(NULL), draw_area_(NULL), vbox_(NULL), server_edit_(NULL),
+      port_edit_(NULL), peer_list_(NULL), callback_(NULL),
+      server_("localhost") {
+  char buffer[10];
+  sprintfn(buffer, sizeof(buffer), "%i", kDefaultServerPort);
+  port_ = buffer;
+}
+
+GtkMainWnd::~GtkMainWnd() {
+  ASSERT(!IsWindow());
+}
+
+void GtkMainWnd::RegisterObserver(MainWndCallback* callback) {
+  callback_ = callback;
+}
+
+bool GtkMainWnd::IsWindow() {
+  return window_ != NULL && GTK_IS_WINDOW(window_);
+}
+
+void GtkMainWnd::MessageBox(const char* caption, const char* text,
+                            bool is_error) {
+  GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(window_),
+      GTK_DIALOG_DESTROY_WITH_PARENT,
+      is_error ? GTK_MESSAGE_ERROR : GTK_MESSAGE_INFO,
+      GTK_BUTTONS_CLOSE, "%s", text);
+  gtk_window_set_title(GTK_WINDOW(dialog), caption);
+  gtk_dialog_run(GTK_DIALOG(dialog));
+  gtk_widget_destroy(dialog);
+}
+
+MainWindow::UI GtkMainWnd::current_ui() {
+  if (vbox_)
+    return CONNECT_TO_SERVER;
+
+  if (peer_list_)
+    return LIST_PEERS;
+
+  return STREAMING;
+}
+
+webrtc::VideoRendererWrapperInterface* GtkMainWnd::local_renderer() {
+  if (!local_renderer_wrapper_.get())
+    local_renderer_wrapper_  =
+        webrtc::CreateVideoRenderer(new VideoRenderer(this));
+  return local_renderer_wrapper_.get();
+}
+
+webrtc::VideoRendererWrapperInterface*  GtkMainWnd::remote_renderer() {
+  if (!remote_renderer_wrapper_.get())
+    remote_renderer_wrapper_ =
+        webrtc::CreateVideoRenderer(new VideoRenderer(this));
+  return remote_renderer_wrapper_.get();
+}
+
+void GtkMainWnd::QueueUIThreadCallback(int msg_id, void* data) {
+  g_idle_add(HandleUIThreadCallback,
+             new UIThreadCallbackData(callback_, msg_id, data));
+}
+
+bool GtkMainWnd::Create() {
+  ASSERT(window_ == NULL);
+
+  window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+  if (window_) {
+    gtk_window_set_position(GTK_WINDOW(window_), GTK_WIN_POS_CENTER);
+    gtk_window_set_default_size(GTK_WINDOW(window_), 640, 480);
+    gtk_window_set_title(GTK_WINDOW(window_), "PeerConnection client");
+    g_signal_connect(G_OBJECT(window_), "delete-event",
+                     G_CALLBACK(&OnDestroyedCallback), this);
+    g_signal_connect(window_, "key-press-event", G_CALLBACK(OnKeyPressCallback),
+                     this);
+
+    SwitchToConnectUI();
+  }
+
+  return window_ != NULL;
+}
+
+bool GtkMainWnd::Destroy() {
+  if (!IsWindow())
+    return false;
+
+  gtk_widget_destroy(window_);
+  window_ = NULL;
+
+  return true;
+}
+
+void GtkMainWnd::SwitchToConnectUI() {
+  LOG(INFO) << __FUNCTION__;
+
+  ASSERT(IsWindow());
+  ASSERT(vbox_ == NULL);
+
+  gtk_container_set_border_width(GTK_CONTAINER(window_), 10);
+
+  if (peer_list_) {
+    gtk_widget_destroy(peer_list_);
+    peer_list_ = NULL;
+  }
+
+  vbox_ = gtk_vbox_new(FALSE, 5);
+  GtkWidget* valign = gtk_alignment_new(0, 1, 0, 0);
+  gtk_container_add(GTK_CONTAINER(vbox_), valign);
+  gtk_container_add(GTK_CONTAINER(window_), vbox_);
+
+  GtkWidget* hbox = gtk_hbox_new(FALSE, 5);
+
+  GtkWidget* label = gtk_label_new("Server");
+  gtk_container_add(GTK_CONTAINER(hbox), label);
+
+  server_edit_ = gtk_entry_new();
+  gtk_entry_set_text(GTK_ENTRY(server_edit_), server_.c_str());
+  gtk_widget_set_size_request(server_edit_, 400, 30);
+  gtk_container_add(GTK_CONTAINER(hbox), server_edit_);
+
+  port_edit_ = gtk_entry_new();
+  gtk_entry_set_text(GTK_ENTRY(port_edit_), port_.c_str());
+  gtk_widget_set_size_request(port_edit_, 70, 30);
+  gtk_container_add(GTK_CONTAINER(hbox), port_edit_);
+
+  GtkWidget* button = gtk_button_new_with_label("Connect");
+  gtk_widget_set_size_request(button, 70, 30);
+  g_signal_connect(button, "clicked", G_CALLBACK(OnClickedCallback), this);
+  gtk_container_add(GTK_CONTAINER(hbox), button);
+
+  GtkWidget* halign = gtk_alignment_new(1, 0, 0, 0);
+  gtk_container_add(GTK_CONTAINER(halign), hbox);
+  gtk_box_pack_start(GTK_BOX(vbox_), halign, FALSE, FALSE, 0);
+
+  gtk_widget_show_all(window_);
+}
+
+void GtkMainWnd::SwitchToPeerList(const Peers& peers) {
+  LOG(INFO) << __FUNCTION__;
+
+  // Clean up buffers from a potential previous session.
+  local_renderer_wrapper_ = NULL;
+  remote_renderer_wrapper_ = NULL;
+
+  if (!peer_list_) {
+    gtk_container_set_border_width(GTK_CONTAINER(window_), 0);
+    if (vbox_) {
+      gtk_widget_destroy(vbox_);
+      vbox_ = NULL;
+      server_edit_ = NULL;
+      port_edit_ = NULL;
+    } else if (draw_area_) {
+      gtk_widget_destroy(draw_area_);
+      draw_area_ = NULL;
+      draw_buffer_.reset();
+    }
+
+    peer_list_ = gtk_tree_view_new();
+    g_signal_connect(peer_list_, "row-activated",
+                     G_CALLBACK(OnRowActivatedCallback), this);
+    gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(peer_list_), FALSE);
+    InitializeList(peer_list_);
+    gtk_container_add(GTK_CONTAINER(window_), peer_list_);
+    gtk_widget_show_all(window_);
+  } else {
+    GtkListStore* store =
+        GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(peer_list_)));
+    gtk_list_store_clear(store);
+  }
+
+  AddToList(peer_list_, "List of currently connected peers:", -1);
+  for (Peers::const_iterator i = peers.begin(); i != peers.end(); ++i)
+    AddToList(peer_list_, i->second.c_str(), i->first);
+}
+
+void GtkMainWnd::SwitchToStreamingUI() {
+  LOG(INFO) << __FUNCTION__;
+
+  ASSERT(draw_area_ == NULL);
+
+  gtk_container_set_border_width(GTK_CONTAINER(window_), 0);
+  if (peer_list_) {
+    gtk_widget_destroy(peer_list_);
+    peer_list_ = NULL;
+  }
+
+  draw_area_ = gtk_drawing_area_new();
+  gtk_container_add(GTK_CONTAINER(window_), draw_area_);
+
+  gtk_widget_show_all(window_);
+}
+
+void GtkMainWnd::OnDestroyed(GtkWidget* widget, GdkEvent* event) {
+  callback_->Close();
+  window_ = NULL;
+  draw_area_ = NULL;
+  vbox_ = NULL;
+  server_edit_ = NULL;
+  port_edit_ = NULL;
+  peer_list_ = NULL;
+}
+
+void GtkMainWnd::OnClicked(GtkWidget* widget) {
+  server_ = gtk_entry_get_text(GTK_ENTRY(server_edit_));
+  port_ = gtk_entry_get_text(GTK_ENTRY(port_edit_));
+  int port = port_.length() ? atoi(port_.c_str()) : 0;
+  callback_->StartLogin(server_, port);
+}
+
+void GtkMainWnd::OnKeyPress(GtkWidget* widget, GdkEventKey* key) {
+  if (key->type == GDK_KEY_PRESS) {
+    switch (key->keyval) {
+     case GDK_Escape:
+       if (draw_area_) {
+         callback_->DisconnectFromCurrentPeer();
+       } else if (peer_list_) {
+         callback_->DisconnectFromServer();
+       }
+       break;
+
+     case GDK_KP_Enter:
+     case GDK_Return:
+       if (vbox_) {
+         OnClicked(NULL);
+       } else if (peer_list_) {
+         // OnRowActivated will be called automatically when the user
+         // presses enter.
+       }
+       break;
+
+     default:
+       break;
+    }
+  }
+}
+
+void GtkMainWnd::OnRowActivated(GtkTreeView* tree_view, GtkTreePath* path,
+                                GtkTreeViewColumn* column) {
+  ASSERT(peer_list_ != NULL);
+  GtkTreeIter iter;
+  GtkTreeModel* model;
+  GtkTreeSelection* selection =
+      gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
+  if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
+     char* text;
+     int id = -1;
+     gtk_tree_model_get(model, &iter, 0, &text, 1, &id,  -1);
+     if (id != -1)
+       callback_->ConnectToPeer(id);
+     g_free(text);
+  }
+}
+
+void GtkMainWnd::OnRedraw() {
+  gdk_threads_enter();
+
+  VideoRenderer* remote_renderer =
+      static_cast<VideoRenderer*>(remote_renderer_wrapper_->renderer());
+  if (remote_renderer && remote_renderer->image() != NULL &&
+      draw_area_ != NULL) {
+    int width = remote_renderer->width();
+    int height = remote_renderer->height();
+
+    if (!draw_buffer_.get()) {
+      draw_buffer_size_ = (width * height * 4) * 4;
+      draw_buffer_.reset(new uint8[draw_buffer_size_]);
+      gtk_widget_set_size_request(draw_area_, width * 2, height * 2);
+    }
+
+    const uint32* image = reinterpret_cast<const uint32*>(
+        remote_renderer->image());
+    uint32* scaled = reinterpret_cast<uint32*>(draw_buffer_.get());
+    for (int r = 0; r < height; ++r) {
+      for (int c = 0; c < width; ++c) {
+        int x = c * 2;
+        scaled[x] = scaled[x + 1] = image[c];
+      }
+
+      uint32* prev_line = scaled;
+      scaled += width * 2;
+      memcpy(scaled, prev_line, (width * 2) * 4);
+
+      image += width;
+      scaled += width * 2;
+    }
+
+    VideoRenderer* local_renderer =
+        static_cast<VideoRenderer*>(local_renderer_wrapper_->renderer());
+    if (local_renderer && local_renderer->image()) {
+      image = reinterpret_cast<const uint32*>(local_renderer->image());
+      scaled = reinterpret_cast<uint32*>(draw_buffer_.get());
+      // Position the local preview on the right side.
+      scaled += (width * 2) - (local_renderer->width() / 2);
+      // right margin...
+      scaled -= 10;
+      // ... towards the bottom.
+      scaled += (height * width * 4) -
+                ((local_renderer->height() / 2) *
+                 (local_renderer->width() / 2) * 4);
+      // bottom margin...
+      scaled -= (width * 2) * 5;
+      for (int r = 0; r < local_renderer->height(); r += 2) {
+        for (int c = 0; c < local_renderer->width(); c += 2) {
+          scaled[c / 2] = image[c + r * local_renderer->width()];
+        }
+        scaled += width * 2;
+      }
+    }
+
+    gdk_draw_rgb_32_image(draw_area_->window,
+                          draw_area_->style->fg_gc[GTK_STATE_NORMAL],
+                          0,
+                          0,
+                          width * 2,
+                          height * 2,
+                          GDK_RGB_DITHER_MAX,
+                          draw_buffer_.get(),
+                          (width * 2) * 4);
+  }
+
+  gdk_threads_leave();
+}
+
+GtkMainWnd::VideoRenderer::VideoRenderer(GtkMainWnd* main_wnd)
+    : width_(0), height_(0), main_wnd_(main_wnd) {
+}
+
+GtkMainWnd::VideoRenderer::~VideoRenderer() {
+}
+
+bool GtkMainWnd::VideoRenderer::SetSize(int width, int height, int reserved) {
+  gdk_threads_enter();
+  width_ = width;
+  height_ = height;
+  image_.reset(new uint8[width * height * 4]);
+  gdk_threads_leave();
+  return true;
+}
+
+bool GtkMainWnd::VideoRenderer::RenderFrame(const cricket::VideoFrame* frame) {
+  gdk_threads_enter();
+
+  int size = width_ * height_ * 4;
+  frame->ConvertToRgbBuffer(cricket::FOURCC_ARGB,
+                            image_.get(),
+                            size,
+                            width_ * 4);
+  // Convert the B,G,R,A frame to R,G,B,A, which is accepted by GTK.
+  // The 'A' is just padding for GTK, so we can use it as temp.
+  uint8* pix = image_.get();
+  uint8* end = image_.get() + size;
+  while (pix < end) {
+    pix[3] = pix[0];     // Save B to A.
+    pix[0] = pix[2];  // Set Red.
+    pix[2] = pix[3];  // Set Blue.
+    pix[3] = 0xFF;     // Fixed Alpha.
+    pix += 4;
+  }
+
+  gdk_threads_leave();
+
+  g_idle_add(Redraw, main_wnd_);
+
+  return true;
+}
+
+
diff --git a/talk/examples/peerconnection/client/linux/main_wnd.h b/talk/examples/peerconnection/client/linux/main_wnd.h
new file mode 100644
index 0000000..c0f020a
--- /dev/null
+++ b/talk/examples/peerconnection/client/linux/main_wnd.h
@@ -0,0 +1,134 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+
+#ifndef PEERCONNECTION_SAMPLES_CLIENT_LINUX_MAIN_WND_H_
+#define PEERCONNECTION_SAMPLES_CLIENT_LINUX_MAIN_WND_H_
+
+#include "talk/examples/peerconnection/client/main_wnd.h"
+#include "talk/examples/peerconnection/client/peer_connection_client.h"
+
+// Forward declarations.
+typedef struct _GtkWidget GtkWidget;
+typedef union _GdkEvent GdkEvent;
+typedef struct _GdkEventKey GdkEventKey;
+typedef struct _GtkTreeView GtkTreeView;
+typedef struct _GtkTreePath GtkTreePath;
+typedef struct _GtkTreeViewColumn GtkTreeViewColumn;
+
+// Implements the main UI of the peer connection client.
+// This is functionally equivalent to the MainWnd class in the Windows
+// implementation.
+class GtkMainWnd : public MainWindow {
+ public:
+  GtkMainWnd();
+  ~GtkMainWnd();
+
+  virtual void RegisterObserver(MainWndCallback* callback);
+  virtual bool IsWindow();
+  virtual void SwitchToConnectUI();
+  virtual void SwitchToPeerList(const Peers& peers);
+  virtual void SwitchToStreamingUI();
+  virtual void MessageBox(const char* caption, const char* text,
+                          bool is_error);
+  virtual MainWindow::UI current_ui();
+  virtual webrtc::VideoRendererWrapperInterface* local_renderer();
+  virtual webrtc::VideoRendererWrapperInterface* remote_renderer();
+
+  virtual void QueueUIThreadCallback(int msg_id, void* data);
+
+  // Creates and shows the main window with the |Connect UI| enabled.
+  bool Create();
+
+  // Destroys the window.  When the window is destroyed, it ends the
+  // main message loop.
+  bool Destroy();
+
+  // Callback for when the main window is destroyed.
+  void OnDestroyed(GtkWidget* widget, GdkEvent* event);
+
+  // Callback for when the user clicks the "Connect" button.
+  void OnClicked(GtkWidget* widget);
+
+  // Callback for keystrokes.  Used to capture Esc and Return.
+  void OnKeyPress(GtkWidget* widget, GdkEventKey* key);
+
+  // Callback when the user double clicks a peer in order to initiate a
+  // connection.
+  void OnRowActivated(GtkTreeView* tree_view, GtkTreePath* path,
+                      GtkTreeViewColumn* column);
+
+  void OnRedraw();
+
+ protected:
+  class VideoRenderer : public cricket::VideoRenderer {
+   public:
+    explicit VideoRenderer(GtkMainWnd* main_wnd);
+    virtual ~VideoRenderer();
+
+    virtual bool SetSize(int width, int height, int reserved);
+
+    virtual bool RenderFrame(const cricket::VideoFrame* frame);
+
+    const uint8* image() const {
+      return image_.get();
+    }
+
+    int width() const {
+      return width_;
+    }
+
+    int height() const {
+      return height_;
+    }
+
+   protected:
+    talk_base::scoped_array<uint8> image_;
+    int width_;
+    int height_;
+    GtkMainWnd* main_wnd_;
+  };
+
+ protected:
+  GtkWidget* window_;  // Our main window.
+  GtkWidget* draw_area_;  // The drawing surface for rendering video streams.
+  GtkWidget* vbox_;  // Container for the Connect UI.
+  GtkWidget* server_edit_;
+  GtkWidget* port_edit_;
+  GtkWidget* peer_list_;  // The list of peers.
+  MainWndCallback* callback_;
+  std::string server_;
+  std::string port_;
+  talk_base::scoped_refptr<webrtc::VideoRendererWrapperInterface>
+      local_renderer_wrapper_;
+  talk_base::scoped_refptr<webrtc::VideoRendererWrapperInterface>
+      remote_renderer_wrapper_;
+  talk_base::scoped_ptr<uint8> draw_buffer_;
+  int draw_buffer_size_;
+};
+
+#endif  // PEERCONNECTION_SAMPLES_CLIENT_LINUX_MAIN_WND_H_
diff --git a/talk/examples/peerconnection/client/main.cc b/talk/examples/peerconnection/client/main.cc
new file mode 100644
index 0000000..4ae0da0
--- /dev/null
+++ b/talk/examples/peerconnection/client/main.cc
@@ -0,0 +1,75 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include <windows.h>
+
+#include "talk/examples/peerconnection/client/conductor.h"
+#include "talk/examples/peerconnection/client/main_wnd.h"
+#include "talk/examples/peerconnection/client/peer_connection_client.h"
+#include "system_wrappers/source/trace_impl.h"
+#include "talk/base/win32socketinit.h"
+
+
+int PASCAL wWinMain(HINSTANCE instance, HINSTANCE prev_instance,
+                    wchar_t* cmd_line, int cmd_show) {
+  talk_base::EnsureWinsockInit();
+
+  webrtc::Trace::CreateTrace();
+  webrtc::Trace::SetTraceFile("peerconnection_client.log");
+  webrtc::Trace::SetLevelFilter(webrtc::kTraceWarning);
+
+  MainWnd wnd;
+  if (!wnd.Create()) {
+    ASSERT(false);
+    return -1;
+  }
+
+  PeerConnectionClient client;
+  Conductor conductor(&client, &wnd);
+
+  // Main loop.
+  MSG msg;
+  BOOL gm;
+  while ((gm = ::GetMessage(&msg, NULL, 0, 0)) && gm != -1) {
+    if (!wnd.PreTranslateMessage(&msg)) {
+      ::TranslateMessage(&msg);
+      ::DispatchMessage(&msg);
+    }
+  }
+
+  if (conductor.connection_active() || client.is_connected()) {
+    while ((conductor.connection_active() || client.is_connected()) &&
+           (gm = ::GetMessage(&msg, NULL, 0, 0)) && gm != -1) {
+      if (!wnd.PreTranslateMessage(&msg)) {
+        ::TranslateMessage(&msg);
+        ::DispatchMessage(&msg);
+      }
+    }
+  }
+
+  return 0;
+}
diff --git a/talk/examples/peerconnection/client/main_wnd.cc b/talk/examples/peerconnection/client/main_wnd.cc
new file mode 100644
index 0000000..e9f79e4
--- /dev/null
+++ b/talk/examples/peerconnection/client/main_wnd.cc
@@ -0,0 +1,599 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include "talk/examples/peerconnection/client/main_wnd.h"
+
+#include <math.h>
+
+#include "talk/base/common.h"
+#include "talk/base/logging.h"
+
+ATOM MainWnd::wnd_class_ = 0;
+const wchar_t MainWnd::kClassName[] = L"WebRTC_MainWnd";
+
+namespace {
+
+const char kConnecting[] = "Connecting... ";
+const char kNoVideoStreams[] = "(no video streams either way)";
+const char kNoIncomingStream[] = "(no incoming video)";
+
+void CalculateWindowSizeForText(HWND wnd, const wchar_t* text,
+                                size_t* width, size_t* height) {
+  HDC dc = ::GetDC(wnd);
+  RECT text_rc = {0};
+  ::DrawText(dc, text, -1, &text_rc, DT_CALCRECT | DT_SINGLELINE);
+  ::ReleaseDC(wnd, dc);
+  RECT client, window;
+  ::GetClientRect(wnd, &client);
+  ::GetWindowRect(wnd, &window);
+
+  *width = text_rc.right - text_rc.left;
+  *width += (window.right - window.left) -
+            (client.right - client.left);
+  *height = text_rc.bottom - text_rc.top;
+  *height += (window.bottom - window.top) -
+             (client.bottom - client.top);
+}
+
+HFONT GetDefaultFont() {
+  static HFONT font = reinterpret_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
+  return font;
+}
+
+std::string GetWindowText(HWND wnd) {
+  char text[MAX_PATH] = {0};
+  ::GetWindowTextA(wnd, &text[0], ARRAYSIZE(text));
+  return text;
+}
+
+void AddListBoxItem(HWND listbox, const std::string& str, LPARAM item_data) {
+  LRESULT index = ::SendMessageA(listbox, LB_ADDSTRING, 0,
+      reinterpret_cast<LPARAM>(str.c_str()));
+  ::SendMessageA(listbox, LB_SETITEMDATA, index, item_data);
+}
+
+}  // namespace
+
+MainWnd::MainWnd()
+  : ui_(CONNECT_TO_SERVER), wnd_(NULL), edit1_(NULL), edit2_(NULL),
+    label1_(NULL), label2_(NULL), button_(NULL), listbox_(NULL),
+    destroyed_(false), callback_(NULL), nested_msg_(NULL) {
+}
+
+MainWnd::~MainWnd() {
+  ASSERT(!IsWindow());
+}
+
+bool MainWnd::Create() {
+  ASSERT(wnd_ == NULL);
+  if (!RegisterWindowClass())
+    return false;
+
+  ui_thread_id_ = ::GetCurrentThreadId();
+  wnd_ = ::CreateWindowExW(WS_EX_OVERLAPPEDWINDOW, kClassName, L"WebRTC",
+      WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN,
+      CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
+      NULL, NULL, GetModuleHandle(NULL), this);
+
+  ::SendMessage(wnd_, WM_SETFONT, reinterpret_cast<WPARAM>(GetDefaultFont()),
+                TRUE);
+
+  CreateChildWindows();
+  SwitchToConnectUI();
+
+  return wnd_ != NULL;
+}
+
+bool MainWnd::Destroy() {
+  BOOL ret = FALSE;
+  if (IsWindow()) {
+    ret = ::DestroyWindow(wnd_);
+  }
+
+  return ret != FALSE;
+}
+
+void MainWnd::RegisterObserver(MainWndCallback* callback) {
+  callback_ = callback;
+}
+
+bool MainWnd::IsWindow() {
+  return wnd_ && ::IsWindow(wnd_) != FALSE;
+}
+
+bool MainWnd::PreTranslateMessage(MSG* msg) {
+  bool ret = false;
+  if (msg->message == WM_CHAR) {
+    if (msg->wParam == VK_TAB) {
+      HandleTabbing();
+      ret = true;
+    } else if (msg->wParam == VK_RETURN) {
+      OnDefaultAction();
+      ret = true;
+    } else if (msg->wParam == VK_ESCAPE) {
+      if (callback_) {
+        if (ui_ == STREAMING) {
+          callback_->DisconnectFromCurrentPeer();
+        } else {
+          callback_->DisconnectFromServer();
+        }
+      }
+    }
+  } else if (msg->hwnd == NULL && msg->message == UI_THREAD_CALLBACK) {
+    callback_->UIThreadCallback(static_cast<int>(msg->wParam),
+                                reinterpret_cast<void*>(msg->lParam));
+    ret = true;
+  }
+  return ret;
+}
+
+void MainWnd::SwitchToConnectUI() {
+  ASSERT(IsWindow());
+  LayoutPeerListUI(false);
+  ui_ = CONNECT_TO_SERVER;
+  LayoutConnectUI(true);
+  ::SetFocus(edit1_);
+}
+
+void MainWnd::SwitchToPeerList(const Peers& peers) {
+  remote_video_.reset();
+  local_video_.reset();
+
+  LayoutConnectUI(false);
+
+  ::SendMessage(listbox_, LB_RESETCONTENT, 0, 0);
+
+  AddListBoxItem(listbox_, "List of currently connected peers:", -1);
+  Peers::const_iterator i = peers.begin();
+  for (; i != peers.end(); ++i)
+    AddListBoxItem(listbox_, i->second.c_str(), i->first);
+
+  ui_ = LIST_PEERS;
+  LayoutPeerListUI(true);
+  ::SetFocus(listbox_);
+}
+
+void MainWnd::SwitchToStreamingUI() {
+  LayoutConnectUI(false);
+  LayoutPeerListUI(false);
+  ui_ = STREAMING;
+}
+
+void MainWnd::MessageBox(const char* caption, const char* text, bool is_error) {
+  DWORD flags = MB_OK;
+  if (is_error)
+    flags |= MB_ICONERROR;
+
+  ::MessageBoxA(handle(), text, caption, flags);
+}
+
+cricket::VideoRenderer* MainWnd::local_renderer() {
+  if (!local_video_.get())
+    local_video_.reset(new VideoRenderer(handle(), 1, 1));
+  return local_video_.get();
+}
+
+cricket::VideoRenderer* MainWnd::remote_renderer() {
+  if (!remote_video_.get())
+    remote_video_.reset(new VideoRenderer(handle(), 1, 1));
+  return remote_video_.get();
+}
+
+void MainWnd::QueueUIThreadCallback(int msg_id, void* data) {
+  ::PostThreadMessage(ui_thread_id_, UI_THREAD_CALLBACK,
+      static_cast<WPARAM>(msg_id), reinterpret_cast<LPARAM>(data));
+}
+
+void MainWnd::OnPaint() {
+  PAINTSTRUCT ps;
+  ::BeginPaint(handle(), &ps);
+
+  RECT rc;
+  ::GetClientRect(handle(), &rc);
+
+  if (ui_ == STREAMING && remote_video_.get() && local_video_.get()) {
+    AutoLock<VideoRenderer> local_lock(local_video_.get());
+    AutoLock<VideoRenderer> remote_lock(remote_video_.get());
+
+    const BITMAPINFO& bmi = remote_video_->bmi();
+    int height = abs(bmi.bmiHeader.biHeight);
+    int width = bmi.bmiHeader.biWidth;
+
+    const uint8* image = remote_video_->image();
+    if (image != NULL) {
+      HDC dc_mem = ::CreateCompatibleDC(ps.hdc);
+      ::SetStretchBltMode(dc_mem, HALFTONE);
+
+      // Set the map mode so that the ratio will be maintained for us.
+      HDC all_dc[] = { ps.hdc, dc_mem };
+      for (int i = 0; i < ARRAY_SIZE(all_dc); ++i) {
+        SetMapMode(all_dc[i], MM_ISOTROPIC);
+        SetWindowExtEx(all_dc[i], width, height, NULL);
+        SetViewportExtEx(all_dc[i], rc.right, rc.bottom, NULL);
+      }
+
+      HBITMAP bmp_mem = ::CreateCompatibleBitmap(ps.hdc, rc.right, rc.bottom);
+      HGDIOBJ bmp_old = ::SelectObject(dc_mem, bmp_mem);
+
+      POINT logical_area = { rc.right, rc.bottom };
+      DPtoLP(ps.hdc, &logical_area, 1);
+
+      HBRUSH brush = ::CreateSolidBrush(RGB(0, 0, 0));
+      RECT logical_rect = {0, 0, logical_area.x, logical_area.y };
+      ::FillRect(dc_mem, &logical_rect, brush);
+      ::DeleteObject(brush);
+
+      int max_unit = std::max(width, height);
+      int x = (logical_area.x / 2) - (width / 2);
+      int y = (logical_area.y / 2) - (height / 2);
+
+      StretchDIBits(dc_mem, x, y, width, height,
+                    0, 0, width, height, image, &bmi, DIB_RGB_COLORS, SRCCOPY);
+
+      if ((rc.right - rc.left) > 200 && (rc.bottom - rc.top) > 200) {
+        const BITMAPINFO& bmi = local_video_->bmi();
+        image = local_video_->image();
+        int thumb_width = bmi.bmiHeader.biWidth / 4;
+        int thumb_height = abs(bmi.bmiHeader.biHeight) / 4;
+        StretchDIBits(dc_mem,
+            logical_area.x - thumb_width - 10,
+            logical_area.y - thumb_height - 10,
+            thumb_width, thumb_height,
+            0, 0, bmi.bmiHeader.biWidth, -bmi.bmiHeader.biHeight,
+            image, &bmi, DIB_RGB_COLORS, SRCCOPY);
+      }
+
+      BitBlt(ps.hdc, 0, 0, logical_area.x, logical_area.y,
+             dc_mem, 0, 0, SRCCOPY);
+
+      // Cleanup.
+      ::SelectObject(dc_mem, bmp_old);
+      ::DeleteObject(bmp_mem);
+      ::DeleteDC(dc_mem);
+    } else {
+      // We're still waiting for the video stream to be initialized.
+      HBRUSH brush = ::CreateSolidBrush(RGB(0, 0, 0));
+      ::FillRect(ps.hdc, &rc, brush);
+      ::DeleteObject(brush);
+
+      HGDIOBJ old_font = ::SelectObject(ps.hdc, GetDefaultFont());
+      ::SetTextColor(ps.hdc, RGB(0xff, 0xff, 0xff));
+      ::SetBkMode(ps.hdc, TRANSPARENT);
+
+      std::string text(kConnecting);
+      if (!local_video_->image()) {
+        text += kNoVideoStreams;
+      } else {
+        text += kNoIncomingStream;
+      }
+      ::DrawTextA(ps.hdc, text.c_str(), -1, &rc,
+          DT_SINGLELINE | DT_CENTER | DT_VCENTER);
+      ::SelectObject(ps.hdc, old_font);
+    }
+  } else {
+    HBRUSH brush = ::CreateSolidBrush(::GetSysColor(COLOR_WINDOW));
+    ::FillRect(ps.hdc, &rc, brush);
+    ::DeleteObject(brush);
+  }
+
+  ::EndPaint(handle(), &ps);
+}
+
+void MainWnd::OnDestroyed() {
+  PostQuitMessage(0);
+}
+
+void MainWnd::OnDefaultAction() {
+  if (!callback_)
+    return;
+  if (ui_ == CONNECT_TO_SERVER) {
+    std::string server(GetWindowText(edit1_));
+    std::string port_str(GetWindowText(edit2_));
+    int port = port_str.length() ? atoi(port_str.c_str()) : 0;
+    callback_->StartLogin(server, port);
+  } else if (ui_ == LIST_PEERS) {
+    LRESULT sel = ::SendMessage(listbox_, LB_GETCURSEL, 0, 0);
+    if (sel != LB_ERR) {
+      LRESULT peer_id = ::SendMessage(listbox_, LB_GETITEMDATA, sel, 0);
+      if (peer_id != -1 && callback_) {
+        callback_->ConnectToPeer(peer_id);
+      }
+    }
+  } else {
+    MessageBoxA(wnd_, "OK!", "Yeah", MB_OK);
+  }
+}
+
+bool MainWnd::OnMessage(UINT msg, WPARAM wp, LPARAM lp, LRESULT* result) {
+  switch (msg) {
+    case WM_ERASEBKGND:
+      *result = TRUE;
+      return true;
+
+    case WM_PAINT:
+      OnPaint();
+      return true;
+
+    case WM_SETFOCUS:
+      if (ui_ == CONNECT_TO_SERVER) {
+        SetFocus(edit1_);
+      } else if (ui_ == LIST_PEERS) {
+        SetFocus(listbox_);
+      }
+      return true;
+
+    case WM_SIZE:
+      if (ui_ == CONNECT_TO_SERVER) {
+        LayoutConnectUI(true);
+      } else if (ui_ == LIST_PEERS) {
+        LayoutPeerListUI(true);
+      }
+      break;
+
+    case WM_CTLCOLORSTATIC:
+      *result = reinterpret_cast<LRESULT>(GetSysColorBrush(COLOR_WINDOW));
+      return true;
+
+    case WM_COMMAND:
+      if (button_ == reinterpret_cast<HWND>(lp)) {
+        if (BN_CLICKED == HIWORD(wp))
+          OnDefaultAction();
+      } else if (listbox_ == reinterpret_cast<HWND>(lp)) {
+        if (LBN_DBLCLK == HIWORD(wp)) {
+          OnDefaultAction();
+        }
+      }
+      return true;
+
+    case WM_CLOSE:
+      if (callback_)
+        callback_->Close();
+      break;
+  }
+  return false;
+}
+
+// static
+LRESULT CALLBACK MainWnd::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
+  MainWnd* me = reinterpret_cast<MainWnd*>(
+      ::GetWindowLongPtr(hwnd, GWL_USERDATA));
+  if (!me && WM_CREATE == msg) {
+    CREATESTRUCT* cs = reinterpret_cast<CREATESTRUCT*>(lp);
+    me = reinterpret_cast<MainWnd*>(cs->lpCreateParams);
+    me->wnd_ = hwnd;
+    ::SetWindowLongPtr(hwnd, GWL_USERDATA, reinterpret_cast<LONG_PTR>(me));
+  }
+
+  LRESULT result = 0;
+  if (me) {
+    void* prev_nested_msg = me->nested_msg_;
+    me->nested_msg_ = &msg;
+
+    bool handled = me->OnMessage(msg, wp, lp, &result);
+    if (WM_NCDESTROY == msg) {
+      me->destroyed_ = true;
+    } else if (!handled) {
+      result = ::DefWindowProc(hwnd, msg, wp, lp);
+    }
+
+    if (me->destroyed_ && prev_nested_msg == NULL) {
+      me->OnDestroyed();
+      me->wnd_ = NULL;
+      me->destroyed_ = false;
+    }
+
+    me->nested_msg_ = prev_nested_msg;
+  } else {
+    result = ::DefWindowProc(hwnd, msg, wp, lp);
+  }
+
+  return result;
+}
+
+// static
+bool MainWnd::RegisterWindowClass() {
+  if (wnd_class_)
+    return true;
+
+  WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
+  wcex.style = CS_DBLCLKS;
+  wcex.hInstance = GetModuleHandle(NULL);
+  wcex.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
+  wcex.hCursor = ::LoadCursor(NULL, IDC_ARROW);
+  wcex.lpfnWndProc = &WndProc;
+  wcex.lpszClassName = kClassName;
+  wnd_class_ = ::RegisterClassEx(&wcex);
+  ASSERT(wnd_class_ != 0);
+  return wnd_class_ != 0;
+}
+
+void MainWnd::CreateChildWindow(HWND* wnd, MainWnd::ChildWindowID id,
+                                const wchar_t* class_name, DWORD control_style,
+                                DWORD ex_style) {
+  if (::IsWindow(*wnd))
+    return;
+
+  // Child windows are invisible at first, and shown after being resized.
+  DWORD style = WS_CHILD | control_style;
+  *wnd = ::CreateWindowEx(ex_style, class_name, L"", style,
+                          100, 100, 100, 100, wnd_,
+                          reinterpret_cast<HMENU>(id),
+                          GetModuleHandle(NULL), NULL);
+  ASSERT(::IsWindow(*wnd) != FALSE);
+  ::SendMessage(*wnd, WM_SETFONT, reinterpret_cast<WPARAM>(GetDefaultFont()),
+                TRUE);
+}
+
+void MainWnd::CreateChildWindows() {
+  // Create the child windows in tab order.
+  CreateChildWindow(&label1_, LABEL1_ID, L"Static", ES_CENTER | ES_READONLY, 0);
+  CreateChildWindow(&edit1_, EDIT_ID, L"Edit",
+                    ES_LEFT | ES_NOHIDESEL | WS_TABSTOP, WS_EX_CLIENTEDGE);
+  CreateChildWindow(&label2_, LABEL2_ID, L"Static", ES_CENTER | ES_READONLY, 0);
+  CreateChildWindow(&edit2_, EDIT_ID, L"Edit",
+                    ES_LEFT | ES_NOHIDESEL | WS_TABSTOP, WS_EX_CLIENTEDGE);
+  CreateChildWindow(&button_, BUTTON_ID, L"Button", BS_CENTER | WS_TABSTOP, 0);
+
+  CreateChildWindow(&listbox_, LISTBOX_ID, L"ListBox",
+                    LBS_HASSTRINGS | LBS_NOTIFY, WS_EX_CLIENTEDGE);
+
+  ::SetWindowTextA(edit1_, GetDefaultServerName().c_str());
+  ::SetWindowTextA(edit2_, "8888");
+}
+
+void MainWnd::LayoutConnectUI(bool show) {
+  struct Windows {
+    HWND wnd;
+    const wchar_t* text;
+    size_t width;
+    size_t height;
+  } windows[] = {
+    { label1_, L"Server" },
+    { edit1_, L"XXXyyyYYYgggXXXyyyYYYggg" },
+    { label2_, L":" },
+    { edit2_, L"XyXyX" },
+    { button_, L"Connect" },
+  };
+
+  if (show) {
+    const size_t kSeparator = 5;
+    size_t total_width = (ARRAYSIZE(windows) - 1) * kSeparator;
+
+    for (size_t i = 0; i < ARRAYSIZE(windows); ++i) {
+      CalculateWindowSizeForText(windows[i].wnd, windows[i].text,
+                                 &windows[i].width, &windows[i].height);
+      total_width += windows[i].width;
+    }
+
+    RECT rc;
+    ::GetClientRect(wnd_, &rc);
+    size_t x = (rc.right / 2) - (total_width / 2);
+    size_t y = rc.bottom / 2;
+    for (size_t i = 0; i < ARRAYSIZE(windows); ++i) {
+      size_t top = y - (windows[i].height / 2);
+      ::MoveWindow(windows[i].wnd, x, top, windows[i].width, windows[i].height,
+                   TRUE);
+      x += kSeparator + windows[i].width;
+      if (windows[i].text[0] != 'X')
+        ::SetWindowText(windows[i].wnd, windows[i].text);
+      ::ShowWindow(windows[i].wnd, SW_SHOWNA);
+    }
+  } else {
+    for (size_t i = 0; i < ARRAYSIZE(windows); ++i) {
+      ::ShowWindow(windows[i].wnd, SW_HIDE);
+    }
+  }
+}
+
+void MainWnd::LayoutPeerListUI(bool show) {
+  if (show) {
+    RECT rc;
+    ::GetClientRect(wnd_, &rc);
+    ::MoveWindow(listbox_, 0, 0, rc.right, rc.bottom, TRUE);
+    ::ShowWindow(listbox_, SW_SHOWNA);
+  } else {
+    ::ShowWindow(listbox_, SW_HIDE);
+    InvalidateRect(wnd_, NULL, TRUE);
+  }
+}
+
+void MainWnd::HandleTabbing() {
+  bool shift = ((::GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0);
+  UINT next_cmd = shift ? GW_HWNDPREV : GW_HWNDNEXT;
+  UINT loop_around_cmd = shift ? GW_HWNDLAST : GW_HWNDFIRST;
+  HWND focus = GetFocus(), next;
+  do {
+    next = ::GetWindow(focus, next_cmd);
+    if (IsWindowVisible(next) &&
+        (GetWindowLong(next, GWL_STYLE) & WS_TABSTOP)) {
+      break;
+    }
+
+    if (!next) {
+      next = ::GetWindow(focus, loop_around_cmd);
+      if (IsWindowVisible(next) &&
+          (GetWindowLong(next, GWL_STYLE) & WS_TABSTOP)) {
+        break;
+      }
+    }
+    focus = next;
+  } while (true);
+  ::SetFocus(next);
+}
+
+//
+// MainWnd::VideoRenderer
+//
+
+MainWnd::VideoRenderer::VideoRenderer(HWND wnd, int width, int height)
+    : wnd_(wnd) {
+  ::InitializeCriticalSection(&buffer_lock_);
+  ZeroMemory(&bmi_, sizeof(bmi_));
+  bmi_.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
+  bmi_.bmiHeader.biPlanes = 1;
+  bmi_.bmiHeader.biBitCount = 32;
+  bmi_.bmiHeader.biCompression = BI_RGB;
+  bmi_.bmiHeader.biWidth = width;
+  bmi_.bmiHeader.biHeight = -height;
+  bmi_.bmiHeader.biSizeImage = width * height *
+                              (bmi_.bmiHeader.biBitCount >> 3);
+}
+
+MainWnd::VideoRenderer::~VideoRenderer() {
+  ::DeleteCriticalSection(&buffer_lock_);
+}
+
+bool MainWnd::VideoRenderer::SetSize(int width, int height, int reserved) {
+  AutoLock<VideoRenderer> lock(this);
+
+  bmi_.bmiHeader.biWidth = width;
+  bmi_.bmiHeader.biHeight = -height;
+  bmi_.bmiHeader.biSizeImage = width * height *
+                               (bmi_.bmiHeader.biBitCount >> 3);
+  image_.reset(new uint8[bmi_.bmiHeader.biSizeImage]);
+
+  return true;
+}
+
+bool MainWnd::VideoRenderer::RenderFrame(const cricket::VideoFrame* frame) {
+  if (!frame)
+    return false;
+
+  {
+    AutoLock<VideoRenderer> lock(this);
+
+    ASSERT(image_.get() != NULL);
+    frame->ConvertToRgbBuffer(cricket::FOURCC_ARGB, image_.get(),
+        bmi_.bmiHeader.biSizeImage,
+        bmi_.bmiHeader.biWidth *
+        (bmi_.bmiHeader.biBitCount >> 3));
+  }
+
+  InvalidateRect(wnd_, NULL, TRUE);
+
+  return true;
+}
diff --git a/talk/examples/peerconnection/client/main_wnd.h b/talk/examples/peerconnection/client/main_wnd.h
new file mode 100644
index 0000000..1807f4e
--- /dev/null
+++ b/talk/examples/peerconnection/client/main_wnd.h
@@ -0,0 +1,208 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef PEERCONNECTION_SAMPLES_CLIENT_MAIN_WND_H_
+#define PEERCONNECTION_SAMPLES_CLIENT_MAIN_WND_H_
+#pragma once
+
+#include <map>
+#include <string>
+
+#include "talk/app/webrtc/mediastream.h"
+#include "talk/examples/peerconnection/client/peer_connection_client.h"
+#include "talk/base/win32.h"
+#include "talk/session/phone/mediachannel.h"
+#include "talk/session/phone/videocommon.h"
+#include "talk/session/phone/videoframe.h"
+#include "talk/session/phone/videorenderer.h"
+
+class MainWndCallback {
+ public:
+  virtual bool StartLogin(const std::string& server, int port) = 0;
+  virtual void DisconnectFromServer() = 0;
+  virtual void ConnectToPeer(int peer_id) = 0;
+  virtual void DisconnectFromCurrentPeer() = 0;
+  virtual void UIThreadCallback(int msg_id, void* data) = 0;
+  virtual void Close() = 0;
+ protected:
+  virtual ~MainWndCallback() {}
+};
+
+// Pure virtual interface for the main window.
+class MainWindow {
+ public:
+  virtual ~MainWindow() {}
+
+  enum UI {
+    CONNECT_TO_SERVER,
+    LIST_PEERS,
+    STREAMING,
+  };
+
+  virtual void RegisterObserver(MainWndCallback* callback) = 0;
+
+  virtual bool IsWindow() = 0;
+  virtual void MessageBox(const char* caption, const char* text,
+                          bool is_error) = 0;
+
+  virtual UI current_ui() = 0;
+
+  virtual void SwitchToConnectUI() = 0;
+  virtual void SwitchToPeerList(const Peers& peers) = 0;
+  virtual void SwitchToStreamingUI() = 0;
+
+  virtual webrtc::VideoRendererWrapperInterface* local_renderer() = 0;
+  virtual webrtc::VideoRendererWrapperInterface* remote_renderer() = 0;
+
+  virtual void QueueUIThreadCallback(int msg_id, void* data) = 0;
+};
+
+#ifdef WIN32
+
+class MainWnd : public MainWindow {
+ public:
+  static const wchar_t kClassName[];
+
+  enum WindowMessages {
+    UI_THREAD_CALLBACK = WM_APP + 1,
+  };
+
+  MainWnd();
+  ~MainWnd();
+
+  bool Create();
+  bool Destroy();
+  bool PreTranslateMessage(MSG* msg);
+
+  virtual void RegisterObserver(MainWndCallback* callback);
+  virtual bool IsWindow();
+  virtual void SwitchToConnectUI();
+  virtual void SwitchToPeerList(const Peers& peers);
+  virtual void SwitchToStreamingUI();
+  virtual void MessageBox(const char* caption, const char* text,
+                          bool is_error);
+  virtual UI current_ui() { return ui_; }
+
+  virtual cricket::VideoRenderer* local_renderer();
+  virtual cricket::VideoRenderer* remote_renderer();
+
+  virtual void QueueUIThreadCallback(int msg_id, void* data);
+
+  HWND handle() const { return wnd_; }
+
+  class VideoRenderer : public cricket::VideoRenderer {
+   public:
+    VideoRenderer(HWND wnd, int width, int height);
+    virtual ~VideoRenderer();
+
+    void Lock() {
+      ::EnterCriticalSection(&buffer_lock_);
+    }
+
+    void Unlock() {
+      ::LeaveCriticalSection(&buffer_lock_);
+    }
+
+    virtual bool SetSize(int width, int height, int reserved);
+
+    // Called when a new frame is available for display.
+    virtual bool RenderFrame(const cricket::VideoFrame* frame);
+
+    const BITMAPINFO& bmi() const { return bmi_; }
+    const uint8* image() const { return image_.get(); }
+
+   protected:
+    enum {
+      SET_SIZE,
+      RENDER_FRAME,
+    };
+
+    HWND wnd_;
+    BITMAPINFO bmi_;
+    talk_base::scoped_array<uint8> image_;
+    CRITICAL_SECTION buffer_lock_;
+  };
+
+  // A little helper class to make sure we always to proper locking and
+  // unlocking when working with VideoRenderer buffers.
+  template <typename T>
+  class AutoLock {
+   public:
+    explicit AutoLock(T* obj) : obj_(obj) { obj_->Lock(); }
+    ~AutoLock() { obj_->Unlock(); }
+   protected:
+    T* obj_;
+  };
+
+ protected:
+  enum ChildWindowID {
+    EDIT_ID = 1,
+    BUTTON_ID,
+    LABEL1_ID,
+    LABEL2_ID,
+    LISTBOX_ID,
+  };
+
+  void OnPaint();
+  void OnDestroyed();
+
+  void OnDefaultAction();
+
+  bool OnMessage(UINT msg, WPARAM wp, LPARAM lp, LRESULT* result);
+
+  static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
+  static bool RegisterWindowClass();
+
+  void CreateChildWindow(HWND* wnd, ChildWindowID id, const wchar_t* class_name,
+                         DWORD control_style, DWORD ex_style);
+  void CreateChildWindows();
+
+  void LayoutConnectUI(bool show);
+  void LayoutPeerListUI(bool show);
+
+  void HandleTabbing();
+
+ private:
+  talk_base::scoped_ptr<VideoRenderer> remote_video_;
+  talk_base::scoped_ptr<VideoRenderer> local_video_;
+  UI ui_;
+  HWND wnd_;
+  DWORD ui_thread_id_;
+  HWND edit1_;
+  HWND edit2_;
+  HWND label1_;
+  HWND label2_;
+  HWND button_;
+  HWND listbox_;
+  bool destroyed_;
+  void* nested_msg_;
+  MainWndCallback* callback_;
+  static ATOM wnd_class_;
+};
+#endif  // WIN32
+
+#endif  // PEERCONNECTION_SAMPLES_CLIENT_MAIN_WND_H_
diff --git a/talk/examples/peerconnection/client/peer_connection_client.cc b/talk/examples/peerconnection/client/peer_connection_client.cc
new file mode 100644
index 0000000..64706de
--- /dev/null
+++ b/talk/examples/peerconnection/client/peer_connection_client.cc
@@ -0,0 +1,499 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include "talk/examples/peerconnection/client/peer_connection_client.h"
+
+#include "talk/examples/peerconnection/client/defaults.h"
+#include "talk/base/common.h"
+#include "talk/base/nethelpers.h"
+#include "talk/base/logging.h"
+#include "talk/base/stringutils.h"
+
+#ifdef WIN32
+#include "talk/base/win32socketserver.h"
+#endif
+
+using talk_base::sprintfn;
+
+namespace {
+
+// This is our magical hangup signal.
+const char kByeMessage[] = "BYE";
+
+talk_base::AsyncSocket* CreateClientSocket() {
+#ifdef WIN32
+  return new talk_base::Win32Socket();
+#elif defined(POSIX)
+  talk_base::Thread* thread = talk_base::Thread::Current();
+  ASSERT(thread != NULL);
+  return thread->socketserver()->CreateAsyncSocket(SOCK_STREAM);
+#else
+#error Platform not supported.
+#endif
+}
+
+}
+
+PeerConnectionClient::PeerConnectionClient()
+  : callback_(NULL),
+    control_socket_(CreateClientSocket()),
+    hanging_get_(CreateClientSocket()),
+    state_(NOT_CONNECTED),
+    my_id_(-1) {
+  control_socket_->SignalCloseEvent.connect(this,
+      &PeerConnectionClient::OnClose);
+  hanging_get_->SignalCloseEvent.connect(this,
+      &PeerConnectionClient::OnClose);
+  control_socket_->SignalConnectEvent.connect(this,
+      &PeerConnectionClient::OnConnect);
+  hanging_get_->SignalConnectEvent.connect(this,
+      &PeerConnectionClient::OnHangingGetConnect);
+  control_socket_->SignalReadEvent.connect(this,
+      &PeerConnectionClient::OnRead);
+  hanging_get_->SignalReadEvent.connect(this,
+      &PeerConnectionClient::OnHangingGetRead);
+}
+
+PeerConnectionClient::~PeerConnectionClient() {
+}
+
+int PeerConnectionClient::id() const {
+  return my_id_;
+}
+
+bool PeerConnectionClient::is_connected() const {
+  return my_id_ != -1;
+}
+
+const Peers& PeerConnectionClient::peers() const {
+  return peers_;
+}
+
+void PeerConnectionClient::RegisterObserver(
+    PeerConnectionClientObserver* callback) {
+  ASSERT(!callback_);
+  callback_ = callback;
+}
+
+bool PeerConnectionClient::Connect(const std::string& server, int port,
+                                   const std::string& client_name) {
+  ASSERT(!server.empty());
+  ASSERT(!client_name.empty());
+
+  if (state_ != NOT_CONNECTED) {
+    LOG(WARNING)
+        << "The client must not be connected before you can call Connect()";
+    return false;
+  }
+
+  if (server.empty() || client_name.empty())
+    return false;
+
+  if (port <= 0)
+    port = kDefaultServerPort;
+
+  server_address_.SetIP(server);
+  server_address_.SetPort(port);
+
+  if (server_address_.IsUnresolved()) {
+    int errcode = 0;
+    hostent* h = talk_base::SafeGetHostByName(
+          server_address_.IPAsString().c_str(), &errcode);
+    if (!h) {
+      LOG(LS_ERROR) << "Failed to resolve host name: "
+                    << server_address_.IPAsString();
+      return false;
+    } else {
+      server_address_.SetResolvedIP(
+          ntohl(*reinterpret_cast<uint32*>(h->h_addr_list[0])));
+      talk_base::FreeHostEnt(h);
+    }
+  }
+
+  char buffer[1024];
+  sprintfn(buffer, sizeof(buffer),
+           "GET /sign_in?%s HTTP/1.0\r\n\r\n", client_name.c_str());
+  onconnect_data_ = buffer;
+
+  bool ret = ConnectControlSocket();
+  if (ret)
+    state_ = SIGNING_IN;
+
+  return ret;
+}
+
+bool PeerConnectionClient::SendToPeer(int peer_id, const std::string& message) {
+  if (state_ != CONNECTED)
+    return false;
+
+  ASSERT(is_connected());
+  ASSERT(control_socket_->GetState() == talk_base::Socket::CS_CLOSED);
+  if (!is_connected() || peer_id == -1)
+    return false;
+
+  char headers[1024];
+  sprintfn(headers, sizeof(headers),
+      "POST /message?peer_id=%i&to=%i HTTP/1.0\r\n"
+      "Content-Length: %i\r\n"
+      "Content-Type: text/plain\r\n"
+      "\r\n",
+      my_id_, peer_id, message.length());
+  onconnect_data_ = headers;
+  onconnect_data_ += message;
+  return ConnectControlSocket();
+}
+
+bool PeerConnectionClient::SendHangUp(int peer_id) {
+  return SendToPeer(peer_id, kByeMessage);
+}
+
+bool PeerConnectionClient::IsSendingMessage() {
+  return state_ == CONNECTED &&
+         control_socket_->GetState() != talk_base::Socket::CS_CLOSED;
+}
+
+bool PeerConnectionClient::SignOut() {
+  if (state_ == NOT_CONNECTED || state_ == SIGNING_OUT)
+    return true;
+
+  if (hanging_get_->GetState() != talk_base::Socket::CS_CLOSED)
+    hanging_get_->Close();
+
+  if (control_socket_->GetState() == talk_base::Socket::CS_CLOSED) {
+    state_ = SIGNING_OUT;
+
+    if (my_id_ != -1) {
+      char buffer[1024];
+      sprintfn(buffer, sizeof(buffer),
+          "GET /sign_out?peer_id=%i HTTP/1.0\r\n\r\n", my_id_);
+      onconnect_data_ = buffer;
+      return ConnectControlSocket();
+    } else {
+      // Can occur if the app is closed before we finish connecting.
+      return true;
+    }
+  } else {
+    state_ = SIGNING_OUT_WAITING;
+  }
+
+  return true;
+}
+
+void PeerConnectionClient::Close() {
+  control_socket_->Close();
+  hanging_get_->Close();
+  onconnect_data_.clear();
+  peers_.clear();
+  my_id_ = -1;
+  state_ = NOT_CONNECTED;
+}
+
+bool PeerConnectionClient::ConnectControlSocket() {
+  ASSERT(control_socket_->GetState() == talk_base::Socket::CS_CLOSED);
+  int err = control_socket_->Connect(server_address_);
+  if (err == SOCKET_ERROR) {
+    Close();
+    return false;
+  }
+  return true;
+}
+
+void PeerConnectionClient::OnConnect(talk_base::AsyncSocket* socket) {
+  ASSERT(!onconnect_data_.empty());
+  size_t sent = socket->Send(onconnect_data_.c_str(), onconnect_data_.length());
+  ASSERT(sent == onconnect_data_.length());
+  UNUSED(sent);
+  onconnect_data_.clear();
+}
+
+void PeerConnectionClient::OnHangingGetConnect(talk_base::AsyncSocket* socket) {
+  char buffer[1024];
+  sprintfn(buffer, sizeof(buffer),
+           "GET /wait?peer_id=%i HTTP/1.0\r\n\r\n", my_id_);
+  int len = strlen(buffer);
+  int sent = socket->Send(buffer, len);
+  ASSERT(sent == len);
+  UNUSED2(sent, len);
+}
+
+void PeerConnectionClient::OnMessageFromPeer(int peer_id,
+                                             const std::string& message) {
+  if (message.length() == (sizeof(kByeMessage) - 1) &&
+      message.compare(kByeMessage) == 0) {
+    callback_->OnPeerDisconnected(peer_id);
+  } else {
+    callback_->OnMessageFromPeer(peer_id, message);
+  }
+}
+
+bool PeerConnectionClient::GetHeaderValue(const std::string& data,
+                                          size_t eoh,
+                                          const char* header_pattern,
+                                          size_t* value) {
+  ASSERT(value != NULL);
+  size_t found = data.find(header_pattern);
+  if (found != std::string::npos && found < eoh) {
+    *value = atoi(&data[found + strlen(header_pattern)]);
+    return true;
+  }
+  return false;
+}
+
+bool PeerConnectionClient::GetHeaderValue(const std::string& data, size_t eoh,
+                                          const char* header_pattern,
+                                          std::string* value) {
+  ASSERT(value != NULL);
+  size_t found = data.find(header_pattern);
+  if (found != std::string::npos && found < eoh) {
+    size_t begin = found + strlen(header_pattern);
+    size_t end = data.find("\r\n", begin);
+    if (end == std::string::npos)
+      end = eoh;
+    value->assign(data.substr(begin, end - begin));
+    return true;
+  }
+  return false;
+}
+
+bool PeerConnectionClient::ReadIntoBuffer(talk_base::AsyncSocket* socket,
+                                          std::string* data,
+                                          size_t* content_length) {
+  LOG(INFO) << __FUNCTION__;
+
+  char buffer[0xffff];
+  do {
+    int bytes = socket->Recv(buffer, sizeof(buffer));
+    if (bytes <= 0)
+      break;
+    data->append(buffer, bytes);
+  } while (true);
+
+  bool ret = false;
+  size_t i = data->find("\r\n\r\n");
+  if (i != std::string::npos) {
+    LOG(INFO) << "Headers received";
+    if (GetHeaderValue(*data, i, "\r\nContent-Length: ", content_length)) {
+      LOG(INFO) << "Expecting " << *content_length << " bytes.";
+      size_t total_response_size = (i + 4) + *content_length;
+      if (data->length() >= total_response_size) {
+        ret = true;
+        std::string should_close;
+        const char kConnection[] = "\r\nConnection: ";
+        if (GetHeaderValue(*data, i, kConnection, &should_close) &&
+            should_close.compare("close") == 0) {
+          socket->Close();
+          // Since we closed the socket, there was no notification delivered
+          // to us.  Compensate by letting ourselves know.
+          OnClose(socket, 0);
+        }
+      } else {
+        // We haven't received everything.  Just continue to accept data.
+      }
+    } else {
+      LOG(LS_ERROR) << "No content length field specified by the server.";
+    }
+  }
+  return ret;
+}
+
+void PeerConnectionClient::OnRead(talk_base::AsyncSocket* socket) {
+  LOG(INFO) << __FUNCTION__;
+  size_t content_length = 0;
+  if (ReadIntoBuffer(socket, &control_data_, &content_length)) {
+    size_t peer_id = 0, eoh = 0;
+    bool ok = ParseServerResponse(control_data_, content_length, &peer_id,
+                                  &eoh);
+    if (ok) {
+      if (my_id_ == -1) {
+        // First response.  Let's store our server assigned ID.
+        ASSERT(state_ == SIGNING_IN);
+        my_id_ = peer_id;
+        ASSERT(my_id_ != -1);
+
+        // The body of the response will be a list of already connected peers.
+        if (content_length) {
+          size_t pos = eoh + 4;
+          while (pos < control_data_.size()) {
+            size_t eol = control_data_.find('\n', pos);
+            if (eol == std::string::npos)
+              break;
+            int id = 0;
+            std::string name;
+            bool connected;
+            if (ParseEntry(control_data_.substr(pos, eol - pos), &name, &id,
+                           &connected) && id != my_id_) {
+              peers_[id] = name;
+              callback_->OnPeerConnected(id, name);
+            }
+            pos = eol + 1;
+          }
+        }
+        ASSERT(is_connected());
+        callback_->OnSignedIn();
+      } else if (state_ == SIGNING_OUT) {
+        Close();
+        callback_->OnDisconnected();
+      } else if (state_ == SIGNING_OUT_WAITING) {
+        SignOut();
+      }
+    }
+
+    control_data_.clear();
+
+    if (state_ == SIGNING_IN) {
+      ASSERT(hanging_get_->GetState() == talk_base::Socket::CS_CLOSED);
+      state_ = CONNECTED;
+      hanging_get_->Connect(server_address_);
+    }
+  }
+}
+
+void PeerConnectionClient::OnHangingGetRead(talk_base::AsyncSocket* socket) {
+  LOG(INFO) << __FUNCTION__;
+  size_t content_length = 0;
+  if (ReadIntoBuffer(socket, &notification_data_, &content_length)) {
+    size_t peer_id = 0, eoh = 0;
+    bool ok = ParseServerResponse(notification_data_, content_length,
+                                  &peer_id, &eoh);
+
+    if (ok) {
+      // Store the position where the body begins.
+      size_t pos = eoh + 4;
+
+      if (my_id_ == static_cast<int>(peer_id)) {
+        // A notification about a new member or a member that just
+        // disconnected.
+        int id = 0;
+        std::string name;
+        bool connected = false;
+        if (ParseEntry(notification_data_.substr(pos), &name, &id,
+                       &connected)) {
+          if (connected) {
+            peers_[id] = name;
+            callback_->OnPeerConnected(id, name);
+          } else {
+            peers_.erase(id);
+            callback_->OnPeerDisconnected(id);
+          }
+        }
+      } else {
+        OnMessageFromPeer(peer_id, notification_data_.substr(pos));
+      }
+    }
+
+    notification_data_.clear();
+  }
+
+  if (hanging_get_->GetState() == talk_base::Socket::CS_CLOSED &&
+      state_ == CONNECTED) {
+    hanging_get_->Connect(server_address_);
+  }
+}
+
+bool PeerConnectionClient::ParseEntry(const std::string& entry,
+                                      std::string* name,
+                                      int* id,
+                                      bool* connected) {
+  ASSERT(name != NULL);
+  ASSERT(id != NULL);
+  ASSERT(connected != NULL);
+  ASSERT(!entry.empty());
+
+  *connected = false;
+  size_t separator = entry.find(',');
+  if (separator != std::string::npos) {
+    *id = atoi(&entry[separator + 1]);
+    name->assign(entry.substr(0, separator));
+    separator = entry.find(',', separator + 1);
+    if (separator != std::string::npos) {
+      *connected = atoi(&entry[separator + 1]) ? true : false;
+    }
+  }
+  return !name->empty();
+}
+
+int PeerConnectionClient::GetResponseStatus(const std::string& response) {
+  int status = -1;
+  size_t pos = response.find(' ');
+  if (pos != std::string::npos)
+    status = atoi(&response[pos + 1]);
+  return status;
+}
+
+bool PeerConnectionClient::ParseServerResponse(const std::string& response,
+                                               size_t content_length,
+                                               size_t* peer_id,
+                                               size_t* eoh) {
+  LOG(INFO) << response;
+
+  int status = GetResponseStatus(response.c_str());
+  if (status != 200) {
+    LOG(LS_ERROR) << "Received error from server";
+    Close();
+    callback_->OnDisconnected();
+    return false;
+  }
+
+  *eoh = response.find("\r\n\r\n");
+  ASSERT(*eoh != std::string::npos);
+  if (*eoh == std::string::npos)
+    return false;
+
+  *peer_id = -1;
+
+  // See comment in peer_channel.cc for why we use the Pragma header and
+  // not e.g. "X-Peer-Id".
+  GetHeaderValue(response, *eoh, "\r\nPragma: ", peer_id);
+
+  return true;
+}
+
+void PeerConnectionClient::OnClose(talk_base::AsyncSocket* socket, int err) {
+  LOG(INFO) << __FUNCTION__;
+
+  socket->Close();
+
+#ifdef WIN32
+  if (err != WSAECONNREFUSED) {
+#else
+  if (err != ECONNREFUSED) {
+#endif
+    if (socket == hanging_get_.get()) {
+      if (state_ == CONNECTED) {
+        LOG(INFO) << "Issuing  a new hanging get";
+        hanging_get_->Close();
+        hanging_get_->Connect(server_address_);
+      }
+    } else {
+      callback_->OnMessageSent(err);
+    }
+  } else {
+    LOG(WARNING) << "Failed to connect to the server";
+    Close();
+    callback_->OnDisconnected();
+  }
+}
diff --git a/talk/examples/peerconnection/client/peer_connection_client.h b/talk/examples/peerconnection/client/peer_connection_client.h
new file mode 100644
index 0000000..f47a24d
--- /dev/null
+++ b/talk/examples/peerconnection/client/peer_connection_client.h
@@ -0,0 +1,126 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef PEERCONNECTION_SAMPLES_CLIENT_PEER_CONNECTION_CLIENT_H_
+#define PEERCONNECTION_SAMPLES_CLIENT_PEER_CONNECTION_CLIENT_H_
+#pragma once
+
+#include <map>
+#include <string>
+
+#include "talk/base/sigslot.h"
+#include "talk/base/physicalsocketserver.h"
+#include "talk/base/scoped_ptr.h"
+
+typedef std::map<int, std::string> Peers;
+
+struct PeerConnectionClientObserver {
+  virtual void OnSignedIn() = 0;  // Called when we're logged on.
+  virtual void OnDisconnected() = 0;
+  virtual void OnPeerConnected(int id, const std::string& name) = 0;
+  virtual void OnPeerDisconnected(int peer_id) = 0;
+  virtual void OnMessageFromPeer(int peer_id, const std::string& message) = 0;
+  virtual void OnMessageSent(int err) = 0;
+
+ protected:
+  virtual ~PeerConnectionClientObserver() {}
+};
+
+class PeerConnectionClient : public sigslot::has_slots<> {
+ public:
+  enum State {
+    NOT_CONNECTED,
+    SIGNING_IN,
+    CONNECTED,
+    SIGNING_OUT_WAITING,
+    SIGNING_OUT,
+  };
+
+  PeerConnectionClient();
+  ~PeerConnectionClient();
+
+  int id() const;
+  bool is_connected() const;
+  const Peers& peers() const;
+
+  void RegisterObserver(PeerConnectionClientObserver* callback);
+
+  bool Connect(const std::string& server, int port,
+               const std::string& client_name);
+
+  bool SendToPeer(int peer_id, const std::string& message);
+  bool SendHangUp(int peer_id);
+  bool IsSendingMessage();
+
+  bool SignOut();
+
+ protected:
+  void Close();
+  bool ConnectControlSocket();
+  void OnConnect(talk_base::AsyncSocket* socket);
+  void OnHangingGetConnect(talk_base::AsyncSocket* socket);
+  void OnMessageFromPeer(int peer_id, const std::string& message);
+
+  // Quick and dirty support for parsing HTTP header values.
+  bool GetHeaderValue(const std::string& data, size_t eoh,
+                      const char* header_pattern, size_t* value);
+
+  bool GetHeaderValue(const std::string& data, size_t eoh,
+                      const char* header_pattern, std::string* value);
+
+  // Returns true if the whole response has been read.
+  bool ReadIntoBuffer(talk_base::AsyncSocket* socket, std::string* data,
+                      size_t* content_length);
+
+  void OnRead(talk_base::AsyncSocket* socket);
+
+  void OnHangingGetRead(talk_base::AsyncSocket* socket);
+
+  // Parses a single line entry in the form "<name>,<id>,<connected>"
+  bool ParseEntry(const std::string& entry, std::string* name, int* id,
+                  bool* connected);
+
+  int GetResponseStatus(const std::string& response);
+
+  bool ParseServerResponse(const std::string& response, size_t content_length,
+                           size_t* peer_id, size_t* eoh);
+
+  void OnClose(talk_base::AsyncSocket* socket, int err);
+
+  PeerConnectionClientObserver* callback_;
+  talk_base::SocketAddress server_address_;
+  talk_base::scoped_ptr<talk_base::AsyncSocket> control_socket_;
+  talk_base::scoped_ptr<talk_base::AsyncSocket> hanging_get_;
+  std::string onconnect_data_;
+  std::string control_data_;
+  std::string notification_data_;
+  Peers peers_;
+  State state_;
+  int my_id_;
+};
+
+#endif  // PEERCONNECTION_SAMPLES_CLIENT_PEER_CONNECTION_CLIENT_H_
diff --git a/talk/examples/peerconnection/server/data_socket.cc b/talk/examples/peerconnection/server/data_socket.cc
new file mode 100644
index 0000000..81cba2e
--- /dev/null
+++ b/talk/examples/peerconnection/server/data_socket.cc
@@ -0,0 +1,304 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include "talk/examples/peerconnection/server/data_socket.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "talk/examples/peerconnection/server/utils.h"
+
+static const char kHeaderTerminator[] = "\r\n\r\n";
+static const int kHeaderTerminatorLength = sizeof(kHeaderTerminator) - 1;
+
+// static
+const char DataSocket::kCrossOriginAllowHeaders[] =
+    "Access-Control-Allow-Origin: *\r\n"
+    "Access-Control-Allow-Credentials: true\r\n"
+    "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n"
+    "Access-Control-Allow-Headers: Content-Type, "
+        "Content-Length, Connection, Cache-Control\r\n"
+    "Access-Control-Expose-Headers: Content-Length, X-Peer-Id\r\n";
+
+#if defined(WIN32)
+class WinsockInitializer {
+  static WinsockInitializer singleton;
+
+  WinsockInitializer() {
+    WSADATA data;
+    WSAStartup(MAKEWORD(1, 0), &data);
+  }
+
+ public:
+  ~WinsockInitializer() { WSACleanup(); }
+};
+WinsockInitializer WinsockInitializer::singleton;
+#endif
+
+//
+// SocketBase
+//
+
+bool SocketBase::Create() {
+  assert(!valid());
+  socket_ = ::socket(AF_INET, SOCK_STREAM, 0);
+  return valid();
+}
+
+void SocketBase::Close() {
+  if (socket_ != INVALID_SOCKET) {
+    closesocket(socket_);
+    socket_ = INVALID_SOCKET;
+  }
+}
+
+//
+// DataSocket
+//
+
+std::string DataSocket::request_arguments() const {
+  size_t args = request_path_.find('?');
+  if (args != std::string::npos)
+    return request_path_.substr(args + 1);
+  return "";
+}
+
+bool DataSocket::PathEquals(const char* path) const {
+  assert(path);
+  size_t args = request_path_.find('?');
+  if (args != std::string::npos)
+    return request_path_.substr(0, args).compare(path) == 0;
+  return request_path_.compare(path) == 0;
+}
+
+bool DataSocket::OnDataAvailable(bool* close_socket) {
+  assert(valid());
+  char buffer[0xfff] = {0};
+  int bytes = recv(socket_, buffer, sizeof(buffer), 0);
+  if (bytes == SOCKET_ERROR || bytes == 0) {
+    *close_socket = true;
+    return false;
+  }
+
+  *close_socket = false;
+
+  bool ret = true;
+  if (headers_received()) {
+    if (method_ != POST) {
+      // unexpectedly received data.
+      ret = false;
+    } else {
+      data_.append(buffer, bytes);
+    }
+  } else {
+    request_headers_.append(buffer, bytes);
+    size_t found = request_headers_.find(kHeaderTerminator);
+    if (found != std::string::npos) {
+      data_ = request_headers_.substr(found + kHeaderTerminatorLength);
+      request_headers_.resize(found + kHeaderTerminatorLength);
+      ret = ParseHeaders();
+    }
+  }
+  return ret;
+}
+
+bool DataSocket::Send(const std::string& data) const {
+  return send(socket_, data.data(), data.length(), 0) != SOCKET_ERROR;
+}
+
+bool DataSocket::Send(const std::string& status, bool connection_close,
+                      const std::string& content_type,
+                      const std::string& extra_headers,
+                      const std::string& data) const {
+  assert(valid());
+  assert(!status.empty());
+  std::string buffer("HTTP/1.1 " + status + "\r\n");
+
+  buffer += "Server: PeerConnectionTestServer/0.1\r\n"
+            "Cache-Control: no-cache\r\n";
+
+  if (connection_close)
+    buffer += "Connection: close\r\n";
+
+  if (!content_type.empty())
+    buffer += "Content-Type: " + content_type + "\r\n";
+
+  buffer += "Content-Length: " + int2str(data.size()) + "\r\n";
+
+  if (!extra_headers.empty()) {
+    buffer += extra_headers;
+    // Extra headers are assumed to have a separator per header.
+  }
+
+  buffer += kCrossOriginAllowHeaders;
+
+  buffer += "\r\n";
+  buffer += data;
+
+  return Send(buffer);
+}
+
+void DataSocket::Clear() {
+  method_ = INVALID;
+  content_length_ = 0;
+  content_type_.clear();
+  request_path_.clear();
+  request_headers_.clear();
+  data_.clear();
+}
+
+bool DataSocket::ParseHeaders() {
+  assert(!request_headers_.empty());
+  assert(method_ == INVALID);
+  size_t i = request_headers_.find("\r\n");
+  if (i == std::string::npos)
+    return false;
+
+  if (!ParseMethodAndPath(request_headers_.data(), i))
+    return false;
+
+  assert(method_ != INVALID);
+  assert(!request_path_.empty());
+
+  if (method_ == POST) {
+    const char* headers = request_headers_.data() + i + 2;
+    size_t len = request_headers_.length() - i - 2;
+    if (!ParseContentLengthAndType(headers, len))
+      return false;
+  }
+
+  return true;
+}
+
+bool DataSocket::ParseMethodAndPath(const char* begin, size_t len) {
+  struct {
+    const char* method_name;
+    size_t method_name_len;
+    RequestMethod id;
+  } supported_methods[] = {
+    { "GET", 3, GET },
+    { "POST", 4, POST },
+    { "OPTIONS", 7, OPTIONS },
+  };
+
+  const char* path = NULL;
+  for (size_t i = 0; i < ARRAYSIZE(supported_methods); ++i) {
+    if (len > supported_methods[i].method_name_len &&
+        isspace(begin[supported_methods[i].method_name_len]) &&
+        strncmp(begin, supported_methods[i].method_name,
+                supported_methods[i].method_name_len) == 0) {
+      method_ = supported_methods[i].id;
+      path = begin + supported_methods[i].method_name_len;
+      break;
+    }
+  }
+
+  const char* end = begin + len;
+  if (!path || path >= end)
+    return false;
+
+  ++path;
+  begin = path;
+  while (!isspace(*path) && path < end)
+    ++path;
+
+  request_path_.assign(begin, path - begin);
+
+  return true;
+}
+
+bool DataSocket::ParseContentLengthAndType(const char* headers, size_t length) {
+  assert(content_length_ == 0);
+  assert(content_type_.empty());
+
+  const char* end = headers + length;
+  while (headers && headers < end) {
+    if (!isspace(headers[0])) {
+      static const char kContentLength[] = "Content-Length:";
+      static const char kContentType[] = "Content-Type:";
+      if ((headers + ARRAYSIZE(kContentLength)) < end &&
+          strncmp(headers, kContentLength,
+                  ARRAYSIZE(kContentLength) - 1) == 0) {
+        headers += ARRAYSIZE(kContentLength) - 1;
+        while (headers[0] == ' ')
+          ++headers;
+        content_length_ = atoi(headers);
+      } else if ((headers + ARRAYSIZE(kContentType)) < end &&
+                 strncmp(headers, kContentType,
+                         ARRAYSIZE(kContentType) - 1) == 0) {
+        headers += ARRAYSIZE(kContentType) - 1;
+        while (headers[0] == ' ')
+          ++headers;
+        const char* type_end = strstr(headers, "\r\n");
+        if (type_end == NULL)
+          type_end = end;
+        content_type_.assign(headers, type_end);
+      }
+    } else {
+      ++headers;
+    }
+    headers = strstr(headers, "\r\n");
+    if (headers)
+      headers += 2;
+  }
+
+  return !content_type_.empty() && content_length_ != 0;
+}
+
+//
+// ListeningSocket
+//
+
+bool ListeningSocket::Listen(unsigned short port) {
+  assert(valid());
+  int enabled = 1;
+  setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR,
+      reinterpret_cast<const char*>(&enabled), sizeof(enabled));
+  struct sockaddr_in addr = {0};
+  addr.sin_family = AF_INET;
+  addr.sin_addr.s_addr = htonl(INADDR_ANY);
+  addr.sin_port = htons(port);
+  if (bind(socket_, reinterpret_cast<const sockaddr*>(&addr),
+           sizeof(addr)) == SOCKET_ERROR) {
+    printf("bind failed\n");
+    return false;
+  }
+  return listen(socket_, 5) != SOCKET_ERROR;
+}
+
+DataSocket* ListeningSocket::Accept() const {
+  assert(valid());
+  struct sockaddr_in addr = {0};
+  socklen_t size = sizeof(addr);
+  int client = accept(socket_, reinterpret_cast<sockaddr*>(&addr), &size);
+  if (client == INVALID_SOCKET)
+    return NULL;
+
+  return new DataSocket(client);
+}
+
diff --git a/talk/examples/peerconnection/server/data_socket.h b/talk/examples/peerconnection/server/data_socket.h
new file mode 100644
index 0000000..b2ba28d
--- /dev/null
+++ b/talk/examples/peerconnection/server/data_socket.h
@@ -0,0 +1,168 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef TALK_EXAMPLES_PEERCONNECTION_SERVER_DATA_SOCKET_H_
+#define TALK_EXAMPLES_PEERCONNECTION_SERVER_DATA_SOCKET_H_
+#pragma once
+
+#ifdef WIN32
+#include <winsock2.h>
+typedef int socklen_t;
+#else
+#include <netinet/in.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#define closesocket close
+#endif
+
+#include <string>
+
+#ifndef SOCKET_ERROR
+#define SOCKET_ERROR (-1)
+#endif
+
+#ifndef INVALID_SOCKET
+#define INVALID_SOCKET  static_cast<int>(~0)
+#endif
+
+class SocketBase {
+ public:
+  SocketBase() : socket_(INVALID_SOCKET) { }
+  explicit SocketBase(int socket) : socket_(socket) { }
+  ~SocketBase() { Close(); }
+
+  int socket() const { return socket_; }
+  bool valid() const { return socket_ != INVALID_SOCKET; }
+
+  bool Create();
+  void Close();
+
+ protected:
+  int socket_;
+};
+
+// Represents an HTTP server socket.
+class DataSocket : public SocketBase {
+ public:
+  enum RequestMethod {
+    INVALID,
+    GET,
+    POST,
+    OPTIONS,
+  };
+
+  explicit DataSocket(int socket)
+      : SocketBase(socket),
+        method_(INVALID),
+        content_length_(0) {
+  }
+
+  ~DataSocket() {
+  }
+
+  static const char kCrossOriginAllowHeaders[];
+
+  bool headers_received() const { return method_ != INVALID; }
+
+  RequestMethod method() const { return method_; }
+
+  const std::string& request_path() const { return request_path_; }
+  std::string request_arguments() const;
+
+  const std::string& data() const { return data_; }
+
+  const std::string& content_type() const { return content_type_; }
+
+  size_t content_length() const { return content_length_; }
+
+  bool request_received() const {
+    return headers_received() && (method_ != POST || data_received());
+  }
+
+  bool data_received() const {
+    return method_ != POST || data_.length() >= content_length_;
+  }
+
+  // Checks if the request path (minus arguments) matches a given path.
+  bool PathEquals(const char* path) const;
+
+  // Called when we have received some data from clients.
+  // Returns false if an error occurred.
+  bool OnDataAvailable(bool* close_socket);
+
+  // Send a raw buffer of bytes.
+  bool Send(const std::string& data) const;
+
+  // Send an HTTP response.  The |status| should start with a valid HTTP
+  // response code, followed by a string.  E.g. "200 OK".
+  // If |connection_close| is set to true, an extra "Connection: close" HTTP
+  // header will be included.  |content_type| is the mime content type, not
+  // including the "Content-Type: " string.
+  // |extra_headers| should be either empty or a list of headers where each
+  // header terminates with "\r\n".
+  // |data| is the body of the message.  It's length will be specified via
+  // a "Content-Length" header.
+  bool Send(const std::string& status, bool connection_close,
+            const std::string& content_type,
+            const std::string& extra_headers, const std::string& data) const;
+
+  // Clears all held state and prepares the socket for receiving a new request.
+  void Clear();
+
+ protected:
+  // A fairly relaxed HTTP header parser.  Parses the method, path and
+  // content length (POST only) of a request.
+  // Returns true if a valid request was received and no errors occurred.
+  bool ParseHeaders();
+
+  // Figures out whether the request is a GET or POST and what path is
+  // being requested.
+  bool ParseMethodAndPath(const char* begin, size_t len);
+
+  // Determines the length of the body and it's mime type.
+  bool ParseContentLengthAndType(const char* headers, size_t length);
+
+ protected:
+  RequestMethod method_;
+  size_t content_length_;
+  std::string content_type_;
+  std::string request_path_;
+  std::string request_headers_;
+  std::string data_;
+};
+
+// The server socket.  Accepts connections and generates DataSocket instances
+// for each new connection.
+class ListeningSocket : public SocketBase {
+ public:
+  ListeningSocket() {}
+
+  bool Listen(unsigned short port);
+  DataSocket* Accept() const;
+};
+
+#endif  // TALK_EXAMPLES_PEERCONNECTION_SERVER_DATA_SOCKET_H_
diff --git a/talk/examples/peerconnection/server/main.cc b/talk/examples/peerconnection/server/main.cc
new file mode 100644
index 0000000..63a573b
--- /dev/null
+++ b/talk/examples/peerconnection/server/main.cc
@@ -0,0 +1,176 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <vector>
+
+#include "talk/examples/peerconnection/server/data_socket.h"
+#include "talk/examples/peerconnection/server/peer_channel.h"
+#include "talk/examples/peerconnection/server/utils.h"
+
+static const size_t kMaxConnections = (FD_SETSIZE - 2);
+
+void HandleBrowserRequest(DataSocket* ds, bool* quit) {
+  assert(ds && ds->valid());
+  assert(quit);
+
+  const std::string& path = ds->request_path();
+
+  *quit = (path.compare("/quit") == 0);
+
+  if (*quit) {
+    ds->Send("200 OK", true, "text/html", "",
+             "<html><body>Quitting...</body></html>");
+  } else if (ds->method() == DataSocket::OPTIONS) {
+    // We'll get this when a browsers do cross-resource-sharing requests.
+    // The headers to allow cross-origin script support will be set inside
+    // Send.
+    ds->Send("200 OK", true, "", "", "");
+  } else {
+    // Here we could write some useful output back to the browser depending on
+    // the path.
+    printf("Received an invalid request: %s\n", ds->request_path().c_str());
+    ds->Send("500 Sorry", true, "text/html", "",
+             "<html><body>Sorry, not yet implemented</body></html>");
+  }
+}
+
+int main(int argc, char** argv) {
+  // TODO: make configurable.
+  static const unsigned short port = 8888;
+
+  ListeningSocket listener;
+  if (!listener.Create()) {
+    printf("Failed to create server socket\n");
+    return -1;
+  } else if (!listener.Listen(port)) {
+    printf("Failed to listen on server socket\n");
+    return -1;
+  }
+
+  printf("Server listening on port %i\n", port);
+
+  PeerChannel clients;
+  typedef std::vector<DataSocket*> SocketArray;
+  SocketArray sockets;
+  bool quit = false;
+  while (!quit) {
+    fd_set socket_set;
+    FD_ZERO(&socket_set);
+    if (listener.valid())
+      FD_SET(listener.socket(), &socket_set);
+
+    for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i)
+      FD_SET((*i)->socket(), &socket_set);
+
+    struct timeval timeout = { 10, 0 };
+    if (select(FD_SETSIZE, &socket_set, NULL, NULL, &timeout) == SOCKET_ERROR) {
+      printf("select failed\n");
+      break;
+    }
+
+    for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i) {
+      DataSocket* s = *i;
+      bool socket_done = true;
+      if (FD_ISSET(s->socket(), &socket_set)) {
+        if (s->OnDataAvailable(&socket_done) && s->request_received()) {
+          ChannelMember* member = clients.Lookup(s);
+          if (member || PeerChannel::IsPeerConnection(s)) {
+            if (!member) {
+              if (s->PathEquals("/sign_in")) {
+                clients.AddMember(s);
+              } else {
+                printf("No member found for: %s\n",
+                    s->request_path().c_str());
+                s->Send("500 Error", true, "text/plain", "",
+                        "Peer most likely gone.");
+              }
+            } else if (member->is_wait_request(s)) {
+              // no need to do anything.
+              socket_done = false;
+            } else {
+              ChannelMember* target = clients.IsTargetedRequest(s);
+              if (target) {
+                member->ForwardRequestToPeer(s, target);
+              } else if (s->PathEquals("/sign_out")) {
+                s->Send("200 OK", true, "text/plain", "", "");
+              } else {
+                printf("Couldn't find target for request: %s\n",
+                    s->request_path().c_str());
+                s->Send("500 Error", true, "text/plain", "",
+                        "Peer most likely gone.");
+              }
+            }
+          } else {
+            HandleBrowserRequest(s, &quit);
+            if (quit) {
+              printf("Quitting...\n");
+              FD_CLR(listener.socket(), &socket_set);
+              listener.Close();
+              clients.CloseAll();
+            }
+          }
+        }
+      } else {
+        socket_done = false;
+      }
+
+      if (socket_done) {
+        printf("Disconnecting socket\n");
+        clients.OnClosing(s);
+        assert(s->valid());  // Close must not have been called yet.
+        FD_CLR(s->socket(), &socket_set);
+        delete (*i);
+        i = sockets.erase(i);
+        if (i == sockets.end())
+          break;
+      }
+    }
+
+    clients.CheckForTimeout();
+
+    if (FD_ISSET(listener.socket(), &socket_set)) {
+      DataSocket* s = listener.Accept();
+      if (sockets.size() >= kMaxConnections) {
+        delete s;  // sorry, that's all we can take.
+        printf("Connection limit reached\n");
+      } else {
+        sockets.push_back(s);
+        printf("New connection...\n");
+      }
+    }
+  }
+
+  for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i)
+    delete (*i);
+  sockets.clear();
+
+  return 0;
+}
diff --git a/talk/examples/peerconnection/server/peer_channel.cc b/talk/examples/peerconnection/server/peer_channel.cc
new file mode 100644
index 0000000..5409941
--- /dev/null
+++ b/talk/examples/peerconnection/server/peer_channel.cc
@@ -0,0 +1,369 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include "talk/examples/peerconnection/server/peer_channel.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <algorithm>
+
+#include "talk/examples/peerconnection/server/data_socket.h"
+#include "talk/examples/peerconnection/server/utils.h"
+
+// Set to the peer id of the originator when messages are being
+// exchanged between peers, but set to the id of the receiving peer
+// itself when notifications are sent from the server about the state
+// of other peers.
+//
+// WORKAROUND: Since support for CORS varies greatly from one browser to the
+// next, we don't use a custom name for our peer-id header (originally it was
+// "X-Peer-Id: ").  Instead, we use a "simple header", "Pragma" which should
+// always be exposed to CORS requests.  There is a special CORS header devoted
+// to exposing proprietary headers (Access-Control-Expose-Headers), however
+// at this point it is not working correctly in some popular browsers.
+static const char kPeerIdHeader[] = "Pragma: ";
+
+static const char* kRequestPaths[] = {
+  "/wait", "/sign_out", "/message",
+};
+
+enum RequestPathIndex {
+  kWait,
+  kSignOut,
+  kMessage,
+};
+
+//
+// ChannelMember
+//
+
+int ChannelMember::s_member_id_ = 0;
+
+ChannelMember::ChannelMember(DataSocket* socket)
+  : waiting_socket_(NULL), id_(++s_member_id_),
+    connected_(true), timestamp_(time(NULL)) {
+  assert(socket);
+  assert(socket->method() == DataSocket::GET);
+  assert(socket->PathEquals("/sign_in"));
+  name_ = socket->request_arguments();  // TODO: urldecode
+  if (!name_.length())
+    name_ = "peer_" + int2str(id_);
+  std::replace(name_.begin(), name_.end(), ',', '_');
+}
+
+ChannelMember::~ChannelMember() {
+}
+
+bool ChannelMember::is_wait_request(DataSocket* ds) const {
+  return ds && ds->PathEquals(kRequestPaths[kWait]);
+}
+
+bool ChannelMember::TimedOut() {
+  return waiting_socket_ == NULL && (time(NULL) - timestamp_) > 30;
+}
+
+std::string ChannelMember::GetPeerIdHeader() const {
+  std::string ret(kPeerIdHeader + int2str(id_) + "\r\n");
+  return ret;
+}
+
+bool ChannelMember::NotifyOfOtherMember(const ChannelMember& other) {
+  assert(&other != this);
+  QueueResponse("200 OK", "text/plain", GetPeerIdHeader(),
+                other.GetEntry());
+  return true;
+}
+
+// Returns a string in the form "name,id\n".
+std::string ChannelMember::GetEntry() const {
+  char entry[1024] = {0};
+  sprintf(entry, "%s,%i,%i\n", name_.c_str(), id_, connected_);  // NOLINT
+  return entry;
+}
+
+void ChannelMember::ForwardRequestToPeer(DataSocket* ds, ChannelMember* peer) {
+  assert(peer);
+  assert(ds);
+
+  std::string extra_headers(GetPeerIdHeader());
+
+  if (peer == this) {
+    ds->Send("200 OK", true, ds->content_type(), extra_headers,
+             ds->data());
+  } else {
+    printf("Client %s sending to %s\n",
+        name_.c_str(), peer->name().c_str());
+    peer->QueueResponse("200 OK", ds->content_type(), extra_headers,
+                        ds->data());
+    ds->Send("200 OK", true, "text/plain", "", "");
+  }
+}
+
+void ChannelMember::OnClosing(DataSocket* ds) {
+  if (ds == waiting_socket_) {
+    waiting_socket_ = NULL;
+    timestamp_ = time(NULL);
+  }
+}
+
+void ChannelMember::QueueResponse(const std::string& status,
+                                  const std::string& content_type,
+                                  const std::string& extra_headers,
+                                  const std::string& data) {
+  if (waiting_socket_) {
+    assert(queue_.size() == 0);
+    assert(waiting_socket_->method() == DataSocket::GET);
+    bool ok = waiting_socket_->Send(status, true, content_type, extra_headers,
+                                    data);
+    if (!ok) {
+      printf("Failed to deliver data to waiting socket\n");
+    }
+    waiting_socket_ = NULL;
+    timestamp_ = time(NULL);
+  } else {
+    QueuedResponse qr;
+    qr.status = status;
+    qr.content_type = content_type;
+    qr.extra_headers = extra_headers;
+    qr.data = data;
+    queue_.push(qr);
+  }
+}
+
+void ChannelMember::SetWaitingSocket(DataSocket* ds) {
+  assert(ds->method() == DataSocket::GET);
+  if (ds && !queue_.empty()) {
+    assert(waiting_socket_ == NULL);
+    const QueuedResponse& response = queue_.back();
+    ds->Send(response.status, true, response.content_type,
+             response.extra_headers, response.data);
+    queue_.pop();
+  } else {
+    waiting_socket_ = ds;
+  }
+}
+
+
+//
+// PeerChannel
+//
+
+// static
+bool PeerChannel::IsPeerConnection(const DataSocket* ds) {
+  assert(ds);
+  return (ds->method() == DataSocket::POST && ds->content_length() > 0) ||
+         (ds->method() == DataSocket::GET && ds->PathEquals("/sign_in"));
+}
+
+ChannelMember* PeerChannel::Lookup(DataSocket* ds) const {
+  assert(ds);
+
+  if (ds->method() != DataSocket::GET && ds->method() != DataSocket::POST)
+    return NULL;
+
+  size_t i = 0;
+  for (; i < ARRAYSIZE(kRequestPaths); ++i) {
+    if (ds->PathEquals(kRequestPaths[i]))
+      break;
+  }
+
+  if (i == ARRAYSIZE(kRequestPaths))
+    return NULL;
+
+  std::string args(ds->request_arguments());
+  static const char kPeerId[] = "peer_id=";
+  size_t found = args.find(kPeerId);
+  if (found == std::string::npos)
+    return NULL;
+
+  int id = atoi(&args[found + ARRAYSIZE(kPeerId) - 1]);
+  Members::const_iterator iter = members_.begin();
+  for (; iter != members_.end(); ++iter) {
+    if (id == (*iter)->id()) {
+      if (i == kWait)
+        (*iter)->SetWaitingSocket(ds);
+      if (i == kSignOut)
+        (*iter)->set_disconnected();
+      return *iter;
+    }
+  }
+
+  return NULL;
+}
+
+ChannelMember* PeerChannel::IsTargetedRequest(const DataSocket* ds) const {
+  assert(ds);
+  // Regardless of GET or POST, we look for the peer_id parameter
+  // only in the request_path.
+  const std::string& path = ds->request_path();
+  size_t args = path.find('?');
+  if (args == std::string::npos)
+    return NULL;
+  size_t found;
+  const char kTargetPeerIdParam[] = "to=";
+  do {
+    found = path.find(kTargetPeerIdParam, args);
+    if (found == std::string::npos)
+      return NULL;
+    if (found == (args + 1) || path[found - 1] == '&') {
+      found += ARRAYSIZE(kTargetPeerIdParam) - 1;
+      break;
+    }
+    args = found + ARRAYSIZE(kTargetPeerIdParam) - 1;
+  } while (true);
+  int id = atoi(&path[found]);
+  Members::const_iterator i = members_.begin();
+  for (; i != members_.end(); ++i) {
+    if ((*i)->id() == id) {
+      return *i;
+    }
+  }
+  return NULL;
+}
+
+bool PeerChannel::AddMember(DataSocket* ds) {
+  assert(IsPeerConnection(ds));
+  ChannelMember* new_guy = new ChannelMember(ds);
+  Members failures;
+  BroadcastChangedState(*new_guy, &failures);
+  HandleDeliveryFailures(&failures);
+  members_.push_back(new_guy);
+
+  printf("New member added (total=%s): %s\n",
+      size_t2str(members_.size()).c_str(), new_guy->name().c_str());
+
+  // Let the newly connected peer know about other members of the channel.
+  std::string content_type;
+  std::string response = BuildResponseForNewMember(*new_guy, &content_type);
+  ds->Send("200 Added", true, content_type, new_guy->GetPeerIdHeader(),
+           response);
+  return true;
+}
+
+void PeerChannel::CloseAll() {
+  Members::const_iterator i = members_.begin();
+  for (; i != members_.end(); ++i) {
+    (*i)->QueueResponse("200 OK", "text/plain", "", "Server shutting down");
+  }
+  DeleteAll();
+}
+
+void PeerChannel::OnClosing(DataSocket* ds) {
+  for (Members::iterator i = members_.begin(); i != members_.end(); ++i) {
+    ChannelMember* m = (*i);
+    m->OnClosing(ds);
+    if (!m->connected()) {
+      i = members_.erase(i);
+      Members failures;
+      BroadcastChangedState(*m, &failures);
+      HandleDeliveryFailures(&failures);
+      delete m;
+      if (i == members_.end())
+        break;
+    }
+  }
+  printf("Total connected: %s\n", size_t2str(members_.size()).c_str());
+}
+
+void PeerChannel::CheckForTimeout() {
+  for (Members::iterator i = members_.begin(); i != members_.end(); ++i) {
+    ChannelMember* m = (*i);
+    if (m->TimedOut()) {
+      printf("Timeout: %s\n", m->name().c_str());
+      m->set_disconnected();
+      i = members_.erase(i);
+      Members failures;
+      BroadcastChangedState(*m, &failures);
+      HandleDeliveryFailures(&failures);
+      delete m;
+      if (i == members_.end())
+        break;
+    }
+  }
+}
+
+void PeerChannel::DeleteAll() {
+  for (Members::iterator i = members_.begin(); i != members_.end(); ++i)
+    delete (*i);
+  members_.clear();
+}
+
+void PeerChannel::BroadcastChangedState(const ChannelMember& member,
+                                        Members* delivery_failures) {
+  // This function should be called prior to DataSocket::Close().
+  assert(delivery_failures);
+
+  if (!member.connected()) {
+    printf("Member disconnected: %s\n", member.name().c_str());
+  }
+
+  Members::iterator i = members_.begin();
+  for (; i != members_.end(); ++i) {
+    if (&member != (*i)) {
+      if (!(*i)->NotifyOfOtherMember(member)) {
+        (*i)->set_disconnected();
+        delivery_failures->push_back(*i);
+        i = members_.erase(i);
+        if (i == members_.end())
+          break;
+      }
+    }
+  }
+}
+
+void PeerChannel::HandleDeliveryFailures(Members* failures) {
+  assert(failures);
+
+  while (!failures->empty()) {
+    Members::iterator i = failures->begin();
+    ChannelMember* member = *i;
+    assert(!member->connected());
+    failures->erase(i);
+    BroadcastChangedState(*member, failures);
+    delete member;
+  }
+}
+
+// Builds a simple list of "name,id\n" entries for each member.
+std::string PeerChannel::BuildResponseForNewMember(const ChannelMember& member,
+                                                   std::string* content_type) {
+  assert(content_type);
+
+  *content_type = "text/plain";
+  // The peer itself will always be the first entry.
+  std::string response(member.GetEntry());
+  for (Members::iterator i = members_.begin(); i != members_.end(); ++i) {
+    if (member.id() != (*i)->id()) {
+      assert((*i)->connected());
+      response += (*i)->GetEntry();
+    }
+  }
+
+  return response;
+}
diff --git a/talk/examples/peerconnection/server/peer_channel.h b/talk/examples/peerconnection/server/peer_channel.h
new file mode 100644
index 0000000..2ecc789
--- /dev/null
+++ b/talk/examples/peerconnection/server/peer_channel.h
@@ -0,0 +1,137 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef TALK_EXAMPLES_PEERCONNECTION_SERVER_PEER_CHANNEL_H_
+#define TALK_EXAMPLES_PEERCONNECTION_SERVER_PEER_CHANNEL_H_
+#pragma once
+
+#include <time.h>
+
+#include <queue>
+#include <string>
+#include <vector>
+
+class DataSocket;
+
+// Represents a single peer connected to the server.
+class ChannelMember {
+ public:
+  explicit ChannelMember(DataSocket* socket);
+  ~ChannelMember();
+
+  bool connected() const { return connected_; }
+  int id() const { return id_; }
+  void set_disconnected() { connected_ = false; }
+  bool is_wait_request(DataSocket* ds) const;
+  const std::string& name() const { return name_; }
+
+  bool TimedOut();
+
+  std::string GetPeerIdHeader() const;
+
+  bool NotifyOfOtherMember(const ChannelMember& other);
+
+  // Returns a string in the form "name,id\n".
+  std::string GetEntry() const;
+
+  void ForwardRequestToPeer(DataSocket* ds, ChannelMember* peer);
+
+  void OnClosing(DataSocket* ds);
+
+  void QueueResponse(const std::string& status, const std::string& content_type,
+                     const std::string& extra_headers, const std::string& data);
+
+  void SetWaitingSocket(DataSocket* ds);
+
+ protected:
+  struct QueuedResponse {
+    std::string status, content_type, extra_headers, data;
+  };
+
+  DataSocket* waiting_socket_;
+  int id_;
+  bool connected_;
+  time_t timestamp_;
+  std::string name_;
+  std::queue<QueuedResponse> queue_;
+  static int s_member_id_;
+};
+
+// Manages all currently connected peers.
+class PeerChannel {
+ public:
+  typedef std::vector<ChannelMember*> Members;
+
+  PeerChannel() {
+  }
+
+  ~PeerChannel() {
+    DeleteAll();
+  }
+
+  const Members& members() const { return members_; }
+
+  // Returns true if the request should be treated as a new ChannelMember
+  // request.  Otherwise the request is not peerconnection related.
+  static bool IsPeerConnection(const DataSocket* ds);
+
+  // Finds a connected peer that's associated with the |ds| socket.
+  ChannelMember* Lookup(DataSocket* ds) const;
+
+  // Checks if the request has a "peer_id" parameter and if so, looks up the
+  // peer for which the request is targeted at.
+  ChannelMember* IsTargetedRequest(const DataSocket* ds) const;
+
+  // Adds a new ChannelMember instance to the list of connected peers and
+  // associates it with the socket.
+  bool AddMember(DataSocket* ds);
+
+  // Closes all connections and sends a "shutting down" message to all
+  // connected peers.
+  void CloseAll();
+
+  // Called when a socket was determined to be closing by the peer (or if the
+  // connection went dead).
+  void OnClosing(DataSocket* ds);
+
+  void CheckForTimeout();
+
+ protected:
+  void DeleteAll();
+  void BroadcastChangedState(const ChannelMember& member,
+                             Members* delivery_failures);
+  void HandleDeliveryFailures(Members* failures);
+
+  // Builds a simple list of "name,id\n" entries for each member.
+  std::string BuildResponseForNewMember(const ChannelMember& member,
+                                        std::string* content_type);
+
+ protected:
+  Members members_;
+};
+
+#endif  // TALK_EXAMPLES_PEERCONNECTION_SERVER_PEER_CHANNEL_H_
diff --git a/talk/examples/peerconnection/server/server_test.html b/talk/examples/peerconnection/server/server_test.html
new file mode 100644
index 0000000..b156dba
--- /dev/null
+++ b/talk/examples/peerconnection/server/server_test.html
@@ -0,0 +1,228 @@
+<html>
+<head>
+<title>PeerConnection server test page</title>
+
+<script>
+var request = null;
+var hangingGet = null;
+var localName;
+var server;
+var my_id = -1;
+var other_peers = {};
+var message_counter = 0;
+
+function trace(txt) {
+  var elem = document.getElementById("debug");
+  elem.innerHTML += txt + "<br>";
+}
+
+function handleServerNotification(data) {
+  trace("Server notification: " + data);
+  var parsed = data.split(',');
+  if (parseInt(parsed[2]) != 0)
+    other_peers[parseInt(parsed[1])] = parsed[0];
+}
+
+function handlePeerMessage(peer_id, data) {
+  ++message_counter;
+  var str = "Message from '" + other_peers[peer_id] + "'&nbsp;";
+  str += "<span id='toggle_" + message_counter + "' onclick='toggleMe(this);' ";
+  str += "style='cursor: pointer'>+</span><br>";
+  str += "<blockquote id='msg_" + message_counter + "' style='display:none'>";
+  str += data + "</blockquote>";
+  trace(str);
+  if (document.getElementById("loopback").checked)
+    sendToPeer(peer_id, data);
+}
+
+function GetIntHeader(r, name) {
+  var val = r.getResponseHeader(name);
+  return val != null && val.length ? parseInt(val) : -1;
+}
+
+function hangingGetCallback() {
+  try {
+    if (hangingGet.readyState != 4)
+      return;
+    if (hangingGet.status != 200) {
+      trace("server error: " + hangingGet.statusText);
+      disconnect();
+    } else {
+      var peer_id = GetIntHeader(hangingGet, "Pragma");
+      if (peer_id == my_id) {
+        handleServerNotification(hangingGet.responseText);
+      } else {
+        handlePeerMessage(peer_id, hangingGet.responseText);
+      }
+    }
+
+    if (hangingGet) {
+      hangingGet.abort();
+      hangingGet = null;
+    }
+
+    if (my_id != -1)
+      window.setTimeout(startHangingGet, 0);
+  } catch (e) {
+    trace("Hanging get error: " + e.description);
+  }
+}
+
+function startHangingGet() {
+  try {
+    hangingGet = new XMLHttpRequest();
+    hangingGet.onreadystatechange = hangingGetCallback;
+    hangingGet.ontimeout = onHangingGetTimeout;
+    hangingGet.open("GET", server + "/wait?peer_id=" + my_id, true);
+    hangingGet.send();  
+  } catch (e) {
+    trace("error" + e.description);
+  }
+}
+
+function onHangingGetTimeout() {
+  trace("hanging get timeout. issuing again.");
+  hangingGet.abort();
+  hangingGet = null;
+  if (my_id != -1)
+    window.setTimeout(startHangingGet, 0);
+}
+
+function signInCallback() {
+  try {
+    if (request.readyState == 4) {
+      if (request.status == 200) {
+        var peers = request.responseText.split("\n");
+        my_id = parseInt(peers[0].split(',')[1]);
+        trace("My id: " + my_id);
+        for (var i = 1; i < peers.length; ++i) {
+          if (peers[i].length > 0) {
+            trace("Peer " + i + ": " + peers[i]);
+            var parsed = peers[i].split(',');
+            other_peers[parseInt(parsed[1])] = parsed[0];
+          }
+        }
+        startHangingGet();
+        request = null;
+      }
+    }
+  } catch (e) {
+    trace("error: " + e.description);
+  }
+}
+
+function signIn() {
+  try {
+    request = new XMLHttpRequest();
+    request.onreadystatechange = signInCallback;
+    request.open("GET", server + "/sign_in?" + localName, true);
+    request.send();
+  } catch (e) {
+    trace("error: " + e.description);
+  }
+}
+
+function sendToPeer(peer_id, data) {
+  if (my_id == -1) {
+    alert("Not connected");
+    return;
+  }
+  if (peer_id == my_id) {
+    alert("Can't send a message to oneself :)");
+    return;
+  }
+  var r = new XMLHttpRequest();
+  r.open("POST", server + "/message?peer_id=" + my_id + "&to=" + peer_id,
+         false);
+  r.setRequestHeader("Content-Type", "text/plain");
+  r.send(data);
+  r = null;
+}
+
+function connect() {
+  localName = document.getElementById("local").value.toLowerCase();
+  server = document.getElementById("server").value.toLowerCase();
+  if (localName.length == 0) {
+    alert("I need a name please.");
+    document.getElementById("local").focus();
+  } else {
+    document.getElementById("connect").disabled = true;
+    document.getElementById("disconnect").disabled = false;
+    document.getElementById("send").disabled = false;
+    signIn();
+  }
+}
+
+function disconnect() {
+  if (request) {
+    request.abort();
+    request = null;
+  }
+  
+  if (hangingGet) {
+    hangingGet.abort();
+    hangingGet = null;
+  }
+
+  if (my_id != -1) {
+    request = new XMLHttpRequest();
+    request.open("GET", server + "/sign_out?peer_id=" + my_id, false);
+    request.send();
+    request = null;
+    my_id = -1;
+  }
+
+  document.getElementById("connect").disabled = false;
+  document.getElementById("disconnect").disabled = true;
+  document.getElementById("send").disabled = true;
+}
+
+window.onbeforeunload = disconnect;
+
+function send() {
+  var text = document.getElementById("message").value;
+  var peer_id = parseInt(document.getElementById("peer_id").value);
+  if (!text.length || peer_id == 0) {
+    alert("No text supplied or invalid peer id");
+  } else {
+    sendToPeer(peer_id, text);
+  }
+}
+
+function toggleMe(obj) {
+  var id = obj.id.replace("toggle", "msg");
+  var t = document.getElementById(id);
+  if (obj.innerText == "+") {
+    obj.innerText = "-";
+    t.style.display = "block";
+  } else {
+    obj.innerText = "+";
+    t.style.display = "none";
+  }
+}
+
+</script>
+
+</head>
+<body>
+Server: <input type="text" id="server" value="http://localhost:8888" /><br>
+<input type="checkbox" id="loopback" checked="checked"/> Loopback (just send
+received messages right back)<br>
+Your name: <input type="text" id="local" value="my_name"/>
+<button id="connect" onclick="connect();">Connect</button>
+<button disabled="true" id="disconnect"
+        onclick="disconnect();">Disconnect</button>
+<br>
+<table><tr><td>
+Target peer id: <input type="text" id="peer_id" size="3"/></td><td>
+Message: <input type="text" id="message"/></td><td>
+<button disabled="true" id="send" onclick="send();">Send</button>
+</td></tr></table>
+<button onclick="document.getElementById('debug').innerHTML='';">
+Clear log</button>
+
+<pre id="debug">
+</pre>
+<br><hr>
+</body>
+</html>
diff --git a/talk/examples/peerconnection/server/utils.cc b/talk/examples/peerconnection/server/utils.cc
new file mode 100644
index 0000000..25ec10c
--- /dev/null
+++ b/talk/examples/peerconnection/server/utils.cc
@@ -0,0 +1,47 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include "talk/examples/peerconnection/server/utils.h"
+
+#include <stdio.h>
+
+std::string int2str(int i) {
+  char buffer[11] = {0};
+  sprintf(buffer, "%d", i);  // NOLINT
+  return buffer;
+}
+
+std::string size_t2str(size_t i) {
+  char buffer[32] = {0};
+#ifdef WIN32
+  // %zu isn't supported on Windows.
+  sprintf(buffer, "%Iu", i);  // NOLINT
+#else
+  sprintf(buffer, "%zu", i);  // NOLINT
+#endif
+  return buffer;
+}
diff --git a/talk/examples/peerconnection/server/utils.h b/talk/examples/peerconnection/server/utils.h
new file mode 100644
index 0000000..d05a2c3
--- /dev/null
+++ b/talk/examples/peerconnection/server/utils.h
@@ -0,0 +1,53 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef TALK_EXAMPLES_PEERCONNECTION_SERVER_UTILS_H_
+#define TALK_EXAMPLES_PEERCONNECTION_SERVER_UTILS_H_
+#pragma once
+
+#ifndef assert
+#ifndef WIN32
+#include <assert.h>
+#else
+#ifndef NDEBUG
+#define assert(expr)  ((void)((expr) ? true : __debugbreak()))
+#else
+#define assert(expr)  ((void)0)
+#endif  // NDEBUG
+#endif  // WIN32
+#endif  // assert
+
+#include <string>
+
+#ifndef ARRAYSIZE
+#define ARRAYSIZE(x) (sizeof(x) / sizeof(x[0]))
+#endif
+
+std::string int2str(int i);
+std::string size_t2str(size_t i);
+
+#endif  // TALK_EXAMPLES_PEERCONNECTION_SERVER_UTILS_H_
diff --git a/talk/examples/plus/libjingleplus.cc b/talk/examples/plus/libjingleplus.cc
new file mode 100644
index 0000000..4d27dfd
--- /dev/null
+++ b/talk/examples/plus/libjingleplus.cc
@@ -0,0 +1,736 @@
+/*
+ * libjingle
+ * Copyright 2006, 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.
+ */
+
+#include <iostream>
+#include "libjingleplus.h"
+#ifdef WIN32
+#include "talk/base/win32socketserver.h"
+#endif
+#include "talk/base/physicalsocketserver.h"
+#include "talk/base/logging.h"
+#include "talk/examples/login/xmppauth.h"
+#include "talk/examples/login/xmppsocket.h"
+#include "talk/examples/login/xmpppump.h"
+#include "presencepushtask.h"
+#include "talk/app/status.h"
+#include "talk/app/message.h"
+#include "rostertask.h"
+#include "talk/app/iqtask.h"
+#include "talk/app/presenceouttask.h"
+#include "talk/app/receivemessagetask.h"
+#include "talk/app/rostersettask.h"
+#include "talk/app/sendmessagetask.h"
+
+enum {
+  MSG_START,
+  
+  // main thread to worker
+  MSG_LOGIN,
+  MSG_DISCONNECT,
+  MSG_SEND_PRESENCE,
+  MSG_SEND_DIRECTED_PRESENCE,
+  MSG_SEND_DIRECTED_MUC_PRESENCE,
+  MSG_SEND_XMPP_MESSAGE,
+  MSG_SEND_XMPP_IQ,
+  MSG_UPDATE_ROSTER_ITEM,
+  MSG_REMOVE_ROSTER_ITEM,
+
+  // worker thread to main thread
+  MSG_STATE_CHANGE,
+  MSG_STATUS_UPDATE,
+  MSG_STATUS_ERROR,
+  MSG_ROSTER_REFRESH_STARTED,
+  MSG_ROSTER_REFRESH_FINISHED,
+  MSG_ROSTER_ITEM_UPDATED,
+  MSG_ROSTER_ITEM_REMOVED,
+  MSG_ROSTER_SUBSCRIBE,
+  MSG_ROSTER_UNSUBSCRIBE,
+  MSG_ROSTER_SUBSCRIBED,
+  MSG_ROSTER_UNSUBSCRIBED,
+  MSG_INCOMING_MESSAGE,
+  MSG_IQ_COMPLETE,
+  MSG_XMPP_INPUT,
+  MSG_XMPP_OUTPUT
+};
+
+class LibjinglePlusWorker : public talk_base::MessageHandler, 
+			    public XmppPumpNotify,
+                            public sigslot::has_slots<> {
+ public:	
+  LibjinglePlusWorker(LibjinglePlus *ljp, LibjinglePlusNotify *notify) : 
+    worker_thread_(NULL), ljp_(ljp), notify_(notify),
+    ppt_(NULL), rmt_(NULL), rt_(NULL), is_test_login_(false) {
+
+    main_thread_.reset(new talk_base::AutoThread());
+#ifdef WIN32
+    ss_.reset(new talk_base::Win32SocketServer(main_thread_.get()));
+    main_thread_->set_socketserver(ss_.get());
+#endif
+
+    pump_.reset(new XmppPump(this));
+
+    pump_->client()->SignalLogInput.connect(this, &LibjinglePlusWorker::OnInputDebug);
+    pump_->client()->SignalLogOutput.connect(this, &LibjinglePlusWorker::OnOutputDebug);
+    //pump_->client()->SignalStateChange.connect(this, &LibjinglePlusWorker::OnStateChange);
+    }
+
+  ~LibjinglePlusWorker() {
+    if (worker_thread_) {
+      worker_thread_->Send(this, MSG_DISCONNECT);
+      delete worker_thread_;
+    }
+  }
+  
+  virtual void OnMessage(talk_base::Message *msg) {
+    switch (msg->message_id) {
+    case MSG_START:
+      LoginW();
+      break;
+    case MSG_DISCONNECT:
+      DisconnectW();
+      break;
+    case MSG_SEND_XMPP_MESSAGE:
+      SendXmppMessageW(static_cast<SendMessageData*>(msg->pdata)->m_);
+      delete msg->pdata;
+      break;
+    case MSG_SEND_XMPP_IQ:
+      SendXmppIqW(static_cast<SendIqData*>(msg->pdata)->to_jid_,
+                  static_cast<SendIqData*>(msg->pdata)->is_get_,
+                  static_cast<SendIqData*>(msg->pdata)->xml_element_);
+      delete msg->pdata;
+      break;
+    case MSG_SEND_PRESENCE:
+      SendPresenceW(static_cast<SendPresenceData*>(msg->pdata)->s_);
+      delete msg->pdata;
+      break;
+    case MSG_SEND_DIRECTED_PRESENCE:
+      SendDirectedPresenceW(static_cast<SendDirectedPresenceData*>(msg->pdata)->j_,
+			    static_cast<SendDirectedPresenceData*>(msg->pdata)->s_);
+      delete msg->pdata;
+      break;
+    case MSG_SEND_DIRECTED_MUC_PRESENCE:
+      SendDirectedMUCPresenceW(static_cast<SendDirectedMUCPresenceData*>(msg->pdata)->j_,
+			       static_cast<SendDirectedMUCPresenceData*>(msg->pdata)->s_,
+			       static_cast<SendDirectedMUCPresenceData*>(msg->pdata)->un_,
+			       static_cast<SendDirectedMUCPresenceData*>(msg->pdata)->ac_,
+			       static_cast<SendDirectedMUCPresenceData*>(msg->pdata)->am_,
+			       static_cast<SendDirectedMUCPresenceData*>(msg->pdata)->role_);
+      delete msg->pdata;
+      break;
+    case MSG_UPDATE_ROSTER_ITEM:
+      UpdateRosterItemW(static_cast<UpdateRosterItemData*>(msg->pdata)->jid_,
+			static_cast<UpdateRosterItemData*>(msg->pdata)->n_,
+			static_cast<UpdateRosterItemData*>(msg->pdata)->g_,
+			static_cast<UpdateRosterItemData*>(msg->pdata)->grt_);
+      delete msg->pdata;
+      break;
+    case MSG_REMOVE_ROSTER_ITEM:
+      RemoveRosterItemW(static_cast<JidData*>(msg->pdata)->jid_);
+      delete msg->pdata;
+      break;
+
+
+
+
+    case MSG_STATUS_UPDATE:
+      OnStatusUpdateW(static_cast<SendPresenceData*>(msg->pdata)->s_);
+      delete msg->pdata;
+      break;
+    case MSG_STATUS_ERROR:
+      OnStatusErrorW(static_cast<StatusErrorData*>(msg->pdata)->stanza_);
+      delete msg->pdata;
+      break;
+    case MSG_STATE_CHANGE:
+      OnStateChangeW(static_cast<StateChangeData*>(msg->pdata)->s_);
+      delete msg->pdata;
+      break;
+    case MSG_ROSTER_REFRESH_STARTED:
+      OnRosterRefreshStartedW();
+      break;
+    case MSG_ROSTER_REFRESH_FINISHED:
+      OnRosterRefreshFinishedW();
+      break;
+    case MSG_ROSTER_ITEM_UPDATED:
+      OnRosterItemUpdatedW(static_cast<RosterItemData*>(msg->pdata)->ri_);
+      delete msg->pdata;
+      break;
+    case MSG_ROSTER_ITEM_REMOVED:
+      OnRosterItemRemovedW(static_cast<RosterItemData*>(msg->pdata)->ri_);
+      delete msg->pdata;
+      break;
+    case MSG_ROSTER_SUBSCRIBE:
+      OnRosterSubscribeW(static_cast<JidData*>(msg->pdata)->jid_);
+      delete msg->pdata;
+      break;
+    case MSG_ROSTER_UNSUBSCRIBE:
+      OnRosterUnsubscribeW(static_cast<JidData*>(msg->pdata)->jid_);
+      delete msg->pdata;
+      break;
+    case MSG_ROSTER_SUBSCRIBED:
+      OnRosterSubscribedW(static_cast<JidData*>(msg->pdata)->jid_);
+      delete msg->pdata;
+      break;
+    case MSG_ROSTER_UNSUBSCRIBED:
+      OnRosterUnsubscribedW(static_cast<JidData*>(msg->pdata)->jid_);
+      delete msg->pdata;
+      break;
+    case MSG_INCOMING_MESSAGE:
+      OnIncomingMessageW(static_cast<XmppMessageData*>(msg->pdata)->m_);
+      delete msg->pdata;
+      break;
+    case MSG_IQ_COMPLETE:
+      OnIqCompleteW(static_cast<IqCompleteData*>(msg->pdata)->success_,
+                    static_cast<IqCompleteData*>(msg->pdata)->stanza_);
+      delete msg->pdata;
+      break;
+    case MSG_XMPP_OUTPUT:
+      OnOutputDebugW(static_cast<StringData*>(msg->pdata)->s_);
+      delete msg->pdata;
+      break;
+    case MSG_XMPP_INPUT:
+      OnInputDebugW(static_cast<StringData*>(msg->pdata)->s_);
+      delete msg->pdata;
+      break;
+    }
+  }
+
+  void Login(const std::string &jid, const std::string &password,
+	     const std::string &machine_address, bool is_test, bool cookie_auth) {
+    is_test_login_ = is_test;
+  
+    xcs_.set_user(jid);
+    if (cookie_auth) {
+	    xcs_.set_auth_cookie(password);
+    } else {
+	    talk_base::InsecureCryptStringImpl pass;
+	    pass.password() = password;
+	    xcs_.set_pass(talk_base::CryptString(pass));
+    }
+    xcs_.set_host(is_test ? "google.com" : "gmail.com");
+    xcs_.set_resource("libjingleplus");
+    xcs_.set_server(talk_base::SocketAddress(machine_address, 5222));
+    xcs_.set_use_tls(!is_test);
+    if (is_test) {
+      xcs_.set_allow_plain(true);
+    }
+
+    worker_thread_ = new talk_base::Thread(&pss_);
+    worker_thread_->Start(); 
+    worker_thread_->Send(this, MSG_START);
+  }
+     
+  void SendXmppMessage(const buzz::XmppMessage &m) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    worker_thread_->Post(this, MSG_SEND_XMPP_MESSAGE, new SendMessageData(m));
+  }
+
+  void SendXmppIq(const buzz::Jid &to_jid, bool is_get,
+                  const buzz::XmlElement *xml_element) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    worker_thread_->Post(this, MSG_SEND_XMPP_IQ,
+                         new SendIqData(to_jid, is_get, xml_element));
+  }
+
+  void SendPresence(const buzz::Status & s) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    worker_thread_->Post(this, MSG_SEND_PRESENCE, new SendPresenceData(s));
+  }
+  
+  void SendDirectedPresence (const buzz::Jid &j, const buzz::Status &s) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    worker_thread_->Post(this, MSG_SEND_DIRECTED_PRESENCE, new SendDirectedPresenceData(j,s));
+  }
+  
+  void SendDirectedMUCPresence(const buzz::Jid &j, const buzz::Status &s,
+			       const std::string &un, const std::string &ac,
+			       const std::string &am, const std::string &role) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    worker_thread_->Post(this, MSG_SEND_DIRECTED_MUC_PRESENCE, new SendDirectedMUCPresenceData(j,s,un,ac,am, role));
+  }
+  
+  void UpdateRosterItem(const buzz::Jid & jid, const std::string & name, 
+			const std::vector<std::string> & groups, buzz::GrType grt) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    worker_thread_->Post(this, MSG_UPDATE_ROSTER_ITEM, new UpdateRosterItemData(jid,name,groups,grt));
+  }
+  
+  void RemoveRosterItemW(const buzz::Jid &jid) {
+    buzz::RosterSetTask *rst = new buzz::RosterSetTask(pump_.get()->client());
+    rst->Remove(jid);
+    rst->Start();
+  }
+
+  void RemoveRosterItem(const buzz::Jid &jid) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    worker_thread_->Post(this, MSG_REMOVE_ROSTER_ITEM, new JidData(jid));
+  }
+  
+  void DoCallbacks() {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    talk_base::Message m;
+    while (main_thread_->Get(&m, 0)) {
+      main_thread_->Dispatch(&m);
+    }
+  }
+
+ private:
+
+  struct UpdateRosterItemData : public talk_base::MessageData {
+    UpdateRosterItemData(const buzz::Jid &jid, const std::string &name, 
+			 const std::vector<std::string> &groups, buzz::GrType grt) :
+      jid_(jid), n_(name), g_(groups), grt_(grt) {}
+    buzz::Jid jid_;
+    std::string n_;
+    std::vector<std::string> g_;
+    buzz::GrType grt_;
+  };
+  
+  void UpdateRosterItemW(const buzz::Jid &jid, const std::string &name,
+			 const std::vector<std::string> &groups, buzz::GrType grt) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    buzz::RosterSetTask *rst = new buzz::RosterSetTask(pump_.get()->client());
+    rst->Update(jid, name, groups, grt);
+    rst->Start();
+  }
+
+  struct StringData : public talk_base::MessageData {
+    StringData(std::string s) : s_(s) {}
+    std::string s_;
+  };
+
+  void OnInputDebugW(const std::string &data) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnXmppInput(data);
+  }
+
+  void OnInputDebug(const char *data, int len) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_XMPP_INPUT, new StringData(std::string(data,len)));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }  
+
+  void OnOutputDebugW(const std::string &data) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnXmppOutput(data);
+  }
+  
+  void OnOutputDebug(const char *data, int len) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_XMPP_OUTPUT, new StringData(std::string(data,len)));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  struct StateChangeData : public talk_base::MessageData {
+    StateChangeData(buzz::XmppEngine::State state) : s_(state) {}
+    buzz::XmppEngine::State s_;
+  };
+  
+  void OnStateChange(buzz::XmppEngine::State state) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    switch (state) {
+    case buzz::XmppEngine::STATE_OPEN:           
+      ppt_ = new buzz::PresencePushTask(pump_.get()->client());
+      ppt_->SignalStatusUpdate.connect(this,
+                                       &LibjinglePlusWorker::OnStatusUpdate);
+      ppt_->SignalStatusError.connect(this,
+                                      &LibjinglePlusWorker::OnStatusError);
+      ppt_->Start();
+
+      rmt_ = new buzz::ReceiveMessageTask(pump_.get()->client(), buzz::XmppEngine::HL_ALL);
+      rmt_->SignalIncomingMessage.connect(this, &LibjinglePlusWorker::OnIncomingMessage);
+      rmt_->Start();
+
+      rt_ = new buzz::RosterTask(pump_.get()->client());
+      rt_->SignalRosterItemUpdated.connect(this, &LibjinglePlusWorker::OnRosterItemUpdated);
+      rt_->SignalRosterItemRemoved.connect(this, &LibjinglePlusWorker::OnRosterItemRemoved);
+      rt_->SignalSubscribe.connect(this, &LibjinglePlusWorker::OnRosterSubscribe);
+      rt_->SignalUnsubscribe.connect(this, &LibjinglePlusWorker::OnRosterUnsubscribe);
+      rt_->SignalSubscribed.connect(this, &LibjinglePlusWorker::OnRosterSubscribed);
+      rt_->SignalUnsubscribed.connect(this, &LibjinglePlusWorker::OnRosterUnsubscribed);
+      rt_->SignalRosterRefreshStarted.connect(this, &LibjinglePlusWorker::OnRosterRefreshStarted);
+      rt_->SignalRosterRefreshFinished.connect(this, &LibjinglePlusWorker::OnRosterRefreshFinished);
+      rt_->Start();
+      rt_->RefreshRosterNow();
+
+      break;
+    }
+    main_thread_->Post(this, MSG_STATE_CHANGE, new StateChangeData(state));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  void OnStateChangeW(buzz::XmppEngine::State state) { 
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnStateChange(state);
+  }
+
+  struct RosterItemData : public talk_base::MessageData {
+    RosterItemData(const buzz::RosterItem &ri) : ri_(ri) {}
+    buzz::RosterItem ri_;
+  };
+
+  void OnRosterItemUpdatedW(const buzz::RosterItem &ri) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnRosterItemUpdated(ri);
+  }
+
+  void OnRosterItemUpdated(const buzz::RosterItem &ri, bool huh) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_ROSTER_ITEM_UPDATED, new RosterItemData(ri));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  void OnRosterItemRemovedW(const buzz::RosterItem &ri) { 
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnRosterItemRemoved(ri);
+  }
+  
+  void OnRosterItemRemoved(const buzz::RosterItem &ri) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_ROSTER_ITEM_REMOVED, new RosterItemData(ri));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  struct JidData : public talk_base::MessageData {
+    JidData(const buzz::Jid& jid) : jid_(jid) {}
+    const buzz::Jid jid_;
+  };
+  
+  void OnRosterSubscribeW(const buzz::Jid& jid) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnRosterSubscribe(jid);
+  }
+
+  void OnRosterSubscribe(const buzz::Jid& jid) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_ROSTER_SUBSCRIBE, new JidData(jid));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  void OnRosterUnsubscribeW(const buzz::Jid &jid) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnRosterUnsubscribe(jid);
+  }
+
+  void OnRosterUnsubscribe(const buzz::Jid &jid) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_ROSTER_UNSUBSCRIBE, new JidData(jid));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+  
+  void OnRosterSubscribedW(const buzz::Jid &jid) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnRosterSubscribed(jid);
+  }
+
+  void OnRosterSubscribed(const buzz::Jid &jid) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_ROSTER_SUBSCRIBED, new JidData(jid));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+  
+  void OnRosterUnsubscribedW(const buzz::Jid &jid) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnRosterUnsubscribed(jid);
+  }
+
+  void OnRosterUnsubscribed(const buzz::Jid &jid) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_ROSTER_UNSUBSCRIBED, new JidData(jid));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  void OnRosterRefreshStartedW() {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnRosterRefreshStarted();
+  }
+  
+  void OnRosterRefreshStarted() {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_ROSTER_REFRESH_STARTED);
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  void OnRosterRefreshFinishedW() {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnRosterRefreshFinished();
+  }
+
+  void OnRosterRefreshFinished() {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_ROSTER_REFRESH_FINISHED);
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+  
+  struct XmppMessageData : talk_base::MessageData {
+    XmppMessageData(const buzz::XmppMessage &m) : m_(m) {}
+    buzz::XmppMessage m_;
+  };
+  
+  void OnIncomingMessageW(const buzz::XmppMessage &msg) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnMessage(msg);
+  }
+
+  void OnIncomingMessage(const buzz::XmppMessage &msg) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_INCOMING_MESSAGE, new XmppMessageData(msg));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  void OnStatusUpdateW (const buzz::Status &status) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnStatusUpdate(status);
+  }
+
+  void OnStatusUpdate (const buzz::Status &status) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_STATUS_UPDATE, new SendPresenceData(status));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  struct StatusErrorData : talk_base::MessageData {
+    StatusErrorData(const buzz::XmlElement &stanza) : stanza_(stanza) {}
+    buzz::XmlElement stanza_;
+  };
+
+  void OnStatusErrorW (const buzz::XmlElement &stanza) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnStatusError(stanza);
+  }
+
+  void OnStatusError (const buzz::XmlElement &stanza) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_STATUS_ERROR, new StatusErrorData(stanza));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  void LoginW() {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    XmppSocket* socket = new XmppSocket(true);
+    pump_->DoLogin(xcs_, socket, is_test_login_ ?  NULL : new XmppAuth());
+    socket->SignalCloseEvent.connect(this,
+        &LibjinglePlusWorker::OnXmppSocketClose);
+  }
+
+  void DisconnectW() {
+    assert(talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    pump_->DoDisconnect();
+  }
+
+  void SendXmppMessageW(const buzz::XmppMessage &m) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    buzz::SendMessageTask * smt = new buzz::SendMessageTask(pump_.get()->client());
+    smt->Send(m);
+    smt->Start();
+  }
+
+  void SendXmppIqW(const buzz::Jid &to_jid, bool is_get,
+                   const buzz::XmlElement *xml_element) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    buzz::IqTask *iq_task = new buzz::IqTask(pump_.get()->client(),
+        is_get, to_jid, const_cast<buzz::XmlElement *>(xml_element));
+    iq_task->SignalDone.connect(this, &LibjinglePlusWorker::OnIqComplete);
+    iq_task->Start();
+  }
+
+ struct IqCompleteData : public talk_base::MessageData {
+   IqCompleteData(bool success, const buzz::XmlElement *stanza) :
+     success_(success), stanza_(*stanza) {}
+   bool success_;
+   buzz::XmlElement stanza_;
+ };
+
+  void OnIqCompleteW(bool success, const buzz::XmlElement& stanza) {
+    assert(talk_base::ThreadManager::CurrentThread() != worker_thread_);
+    if (notify_)
+      notify_->OnIqDone(success, stanza);
+  }
+
+  void OnIqComplete(bool success, const buzz::XmlElement *stanza) {
+    assert(talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    main_thread_->Post(this, MSG_IQ_COMPLETE,
+       new IqCompleteData(success, stanza));
+    if (notify_)
+      notify_->WakeupMainThread();
+  }
+
+  void SendPresenceW(const buzz::Status & s) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    buzz::PresenceOutTask *pot = new buzz::PresenceOutTask(pump_.get()->client());
+    pot->Send(s);
+    pot->Start();
+  }
+
+
+  void SendDirectedMUCPresenceW(const buzz::Jid & j, const buzz::Status & s, 
+			       const std::string &user_nick, const std::string &api_capability,
+			       const std::string &api_message, const std::string &role) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    buzz::PresenceOutTask *pot = new buzz::PresenceOutTask(pump_.get()->client());
+    pot->SendDirectedMUC(j,s,user_nick,api_capability,api_message, role);
+    pot->Start();
+  }
+  
+  void SendDirectedPresenceW(const buzz::Jid & j, const buzz::Status & s) {
+    assert (talk_base::ThreadManager::CurrentThread() == worker_thread_);
+    buzz::PresenceOutTask *pot = new buzz::PresenceOutTask(pump_.get()->client());
+    pot->SendDirected(j,s);
+    pot->Start();
+  }
+
+  void OnXmppSocketClose(int error) {
+    notify_->OnSocketClose(error);
+  }
+
+ struct SendMessageData : public talk_base::MessageData {
+   SendMessageData(const buzz::XmppMessage &m) : m_(m) {}
+   buzz::XmppMessage m_;
+  };
+
+ struct SendIqData : public talk_base::MessageData {
+   SendIqData(const buzz::Jid &jid, bool is_get, const buzz::XmlElement *m)
+     : to_jid_(jid), is_get_(is_get), xml_element_(m) {}
+   buzz::Jid to_jid_;
+   bool is_get_;
+   const buzz::XmlElement *xml_element_;
+  };
+
+ struct SendPresenceData : public talk_base::MessageData {
+   SendPresenceData(const buzz::Status &s) : s_(s) {}
+   buzz::Status s_;
+  };  
+
+ struct SendDirectedPresenceData : public talk_base::MessageData {
+   SendDirectedPresenceData(const buzz::Jid &j, const buzz::Status &s) : j_(j), s_(s) {}
+   buzz::Jid j_;
+   buzz::Status s_;
+ };
+
+  struct SendDirectedMUCPresenceData : public talk_base::MessageData {
+    SendDirectedMUCPresenceData(const buzz::Jid &j, const buzz::Status &s,
+				const std::string &un, const std::string &ac,
+				const std::string &am, const std::string &role)
+      : j_(j), s_(s), un_(un), ac_(ac), am_(am), role_(role) {}
+    buzz::Jid j_;
+    buzz::Status s_;
+    std::string un_;
+    std::string ac_;
+    std::string am_;
+    std::string role_;
+  };
+
+  talk_base::scoped_ptr<talk_base::Win32SocketServer> ss_;
+  talk_base::scoped_ptr<talk_base::Thread> main_thread_;
+  talk_base::Thread *worker_thread_;
+
+  LibjinglePlus *ljp_;
+  LibjinglePlusNotify *notify_;
+  buzz::XmppClientSettings xcs_;
+  talk_base::PhysicalSocketServer pss_;
+
+  talk_base::scoped_ptr<XmppPump> pump_;
+  buzz::PresencePushTask * ppt_;
+  buzz::ReceiveMessageTask * rmt_;
+  buzz::RosterTask * rt_;
+
+  bool is_test_login_;
+};
+
+LibjinglePlus::LibjinglePlus(LibjinglePlusNotify *notify)
+{
+  worker_ = new LibjinglePlusWorker(this, notify);
+}
+
+LibjinglePlus::~LibjinglePlus()
+{
+ delete worker_;
+  worker_ = NULL;
+}
+
+void LibjinglePlus::Login(const std::string &jid,
+		          const std::string &password,
+		          const std::string &machine_address,
+			  bool is_test, bool cookie_auth) {
+  worker_->Login(jid, password, machine_address, is_test, cookie_auth);
+}
+
+void LibjinglePlus::SendPresence(const buzz::Status & s) {
+  worker_->SendPresence(s);
+}
+
+void LibjinglePlus::SendDirectedPresence(const buzz::Jid & j, const buzz::Status & s) {
+  worker_->SendDirectedPresence(j,s);
+}
+
+void LibjinglePlus::SendDirectedMUCPresence(const buzz::Jid & j,
+    const buzz::Status & s, const std::string &user_nick,
+    const std::string &api_capability, const std::string &api_message,
+    const std::string &role) {
+  worker_->SendDirectedMUCPresence(j,s,user_nick,api_capability,api_message,
+      role);
+}
+
+void LibjinglePlus::SendXmppMessage(const buzz::XmppMessage & m) {
+  worker_->SendXmppMessage(m);
+}
+
+void LibjinglePlus::SendXmppIq(const buzz::Jid &to_jid, bool is_get,
+                               const buzz::XmlElement *iq_element) {
+  worker_->SendXmppIq(to_jid, is_get, iq_element);
+}
+
+void LibjinglePlus::DoCallbacks() {
+  worker_->DoCallbacks();
+}
diff --git a/talk/examples/plus/libjingleplus.h b/talk/examples/plus/libjingleplus.h
new file mode 100644
index 0000000..a2898f5
--- /dev/null
+++ b/talk/examples/plus/libjingleplus.h
@@ -0,0 +1,154 @@
+/*
+ * libjingle
+ * Copyright 2006, 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.
+ */
+
+// LibjinglePlus is a class that connects to Google Talk, creates
+// some common tasks, and emits signals when things change
+
+#ifndef LIBJINGLEPLUS_H__
+#define LIBJINGLEPLUS_H__
+
+#include "talk/base/basicdefs.h"
+#include "talk/app/rosteritem.h"
+#include "talk/app/message.h"
+#include "talk/app/status.h"
+#include "talk/xmpp/xmppengine.h"
+#include "talk/base/scoped_ptr.h"
+
+
+class LibjinglePlusWorker;
+
+class LibjinglePlusNotify {
+ public:
+  virtual ~LibjinglePlusNotify() {}
+
+  /* Libjingle+ works on its own thread. It will call WakeupMainThread
+   * when it has something to report. The main thread should then wake up,
+   * and call DoCallbacks on the LibjinglePlus object.
+   *
+   * This function gets called from libjingle+'s worker thread. All other
+   * methods in LibjinglePlusNotify get called from the thread you call
+   * DoCallbacks() on.
+   *
+   * If running on Windows, libjingle+ will use Windows messages to generate
+   * callbacks from the main thread, and you don't need to do anything here.
+   */
+  virtual void WakeupMainThread() = 0;
+
+  /* Connection */
+  /* Called when the connection state changes */
+  virtual void OnStateChange(buzz::XmppEngine::State) = 0;
+
+  /* Called when the socket closes */
+  virtual void OnSocketClose(int error_code) = 0;
+
+  /* Called when XMPP is being sent or received. Used for debugging */
+  virtual void OnXmppOutput(const std::string &output) = 0;
+  virtual void OnXmppInput(const std::string &input) = 0;
+
+  /* Presence */
+  /* Called when someone's Status is updated */
+  virtual void OnStatusUpdate(const buzz::Status &status) = 0;
+
+  /* Called when a status update results in an error */
+  virtual void OnStatusError(const buzz::XmlElement &stanza) = 0;
+
+  /* Called with an IQ return code */
+  virtual void OnIqDone(bool success, const buzz::XmlElement &stanza) = 0;
+
+  /* Message */
+  /* Called when a message comes in. */
+  virtual void OnMessage(const buzz::XmppMessage &message) = 0;
+
+  /* Roster */
+
+  /* Called when we start refreshing the roster */
+  virtual void OnRosterRefreshStarted() = 0;
+  /* Called when we have the entire roster */
+  virtual void OnRosterRefreshFinished() = 0;
+  /* Called when an item on the roster is created or updated */
+  virtual void OnRosterItemUpdated(const buzz::RosterItem &ri) = 0;
+  /* Called when an item on the roster is removed */
+  virtual void OnRosterItemRemoved(const buzz::RosterItem &ri) = 0;
+
+  /* Subscriptions */
+  virtual void OnRosterSubscribe(const buzz::Jid &jid) = 0;
+  virtual void OnRosterUnsubscribe(const buzz::Jid &jid) = 0;
+  virtual void OnRosterSubscribed(const buzz::Jid &jid) = 0;
+  virtual void OnRosterUnsubscribed(const buzz::Jid &jid) = 0;
+
+};
+
+class LibjinglePlus 
+{
+ public:
+  /* Provide the constructor with your interface. */
+  LibjinglePlus(LibjinglePlusNotify *notify);
+  ~LibjinglePlus();
+ 
+  /* Logs in and starts doing stuff 
+   *
+   * If cookie_auth is true, password must be a Gaia SID. Otherwise,
+   * it should be the user's password
+   */
+  void Login(const std::string &username, const std::string &password,
+	     const std::string &machine_address, bool is_test, bool cookie_auth);
+
+  /* Set Presence */
+  void SendPresence(const buzz::Status & s);
+  void SendDirectedPresence(const buzz::Jid & j, const buzz::Status & s);
+  void SendDirectedMUCPresence(const buzz::Jid & j, const buzz::Status & s, 
+		       const std::string &user_nick, const std::string &api_capability,
+		       const std::string &api_message, const std::string &role);
+
+  /* Send Message */
+  void SendXmppMessage(const buzz::XmppMessage & m);
+
+  /* Send IQ */
+  void SendXmppIq(const buzz::Jid &to_jid, bool is_get,
+                  const buzz::XmlElement *iq_element);
+
+  /* Set Roster */
+  void UpdateRosterItem(const buzz::Jid & jid, const std::string & name, 
+			const std::vector<std::string> & groups, buzz::GrType grt);
+  void RemoveRosterItem(const buzz::Jid &jid);
+
+  /* Call this from the thread you want to receive callbacks on. Typically, this will be called
+   * after your WakeupMainThread() notify function is called.
+   *
+   * On Windows, libjingle+ will trigger its callback from the Windows message loop, and
+   * you needn't call this yourself.
+   */
+  void DoCallbacks();
+
+ private:
+  void LoginInternal(const std::string &jid, const std::string &password,
+		     const std::string &machine_address, bool is_test);
+
+  LibjinglePlusWorker *worker_;
+};
+
+#endif  // LIBJINGLE_PLUS_H__
diff --git a/talk/examples/plus/presencepushtask.cc b/talk/examples/plus/presencepushtask.cc
new file mode 100644
index 0000000..034a23c
--- /dev/null
+++ b/talk/examples/plus/presencepushtask.cc
@@ -0,0 +1,203 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#include "talk/base/stringencode.h"
+#include "presencepushtask.h"
+#include "talk/xmpp/constants.h"
+#include <sstream>
+
+
+namespace buzz {
+
+// string helper functions -----------------------------------------------------
+
+static bool
+IsXmlSpace(int ch) {
+  return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
+}
+
+static bool
+ListContainsToken(const std::string & list, const std::string & token) {
+  size_t i = list.find(token);
+  if (i == std::string::npos || token.empty())
+    return false;
+  bool boundary_before = (i == 0 || IsXmlSpace(list[i - 1]));
+  bool boundary_after = (i == list.length() - token.length() || IsXmlSpace(list[i + token.length()]));
+  return boundary_before && boundary_after;
+}
+
+
+bool
+PresencePushTask::HandleStanza(const XmlElement * stanza) {
+  if (stanza->Name() != QN_PRESENCE)
+    return false;
+  if (stanza->HasAttr(QN_TYPE) && stanza->Attr(QN_TYPE) != STR_UNAVAILABLE) {
+    if (stanza->Attr(QN_TYPE) == STR_ERROR) {
+      // Pass on the error.
+      const XmlElement* error_xml_elem = stanza->FirstNamed(QN_ERROR);
+      if (!error_xml_elem) {
+        return false;
+      }
+      SignalStatusError(*error_xml_elem);
+      return true;
+    }
+  }
+  QueueStanza(stanza);
+  return true;
+}
+
+static bool IsUtf8FirstByte(int c) {
+  return (((c)&0x80)==0) || // is single byte
+    ((unsigned char)((c)-0xc0)<0x3e); // or is lead byte
+}
+
+int
+PresencePushTask::ProcessStart() {
+  const XmlElement * stanza = NextStanza();
+  if (stanza == NULL)
+    return STATE_BLOCKED;
+  Status s;
+
+  s.set_jid(Jid(stanza->Attr(QN_FROM)));
+
+  if (stanza->Attr(QN_TYPE) == STR_UNAVAILABLE) {
+    s.set_available(false);
+    SignalStatusUpdate(s);
+  }
+  else {
+    s.set_available(true);
+    const XmlElement * status = stanza->FirstNamed(QN_STATUS);
+    if (status != NULL) {
+      s.set_status(status->BodyText());
+
+      // Truncate status messages longer than 300 bytes
+      if (s.status().length() > 300) {
+        size_t len = 300;
+
+        // Be careful not to split legal utf-8 chars in half
+        while (!IsUtf8FirstByte(s.status()[len]) && len > 0) {
+          len -= 1;
+        }
+        std::string truncated(s.status(), 0, len);
+        s.set_status(truncated);
+      }
+    }
+
+    const XmlElement * priority = stanza->FirstNamed(QN_PRIORITY);
+    if (priority != NULL) {
+      int pri;
+      if (talk_base::FromString(priority->BodyText(), &pri)) {
+        s.set_priority(pri);
+      }
+    }
+
+    const XmlElement * show = stanza->FirstNamed(QN_SHOW);
+    if (show == NULL || show->FirstChild() == NULL) {
+      s.set_show(Status::SHOW_ONLINE);
+    }
+    else {
+      if (show->BodyText() == "away") {
+        s.set_show(Status::SHOW_AWAY);
+      }
+      else if (show->BodyText() == "xa") {
+        s.set_show(Status::SHOW_XA);
+      }
+      else if (show->BodyText() == "dnd") {
+        s.set_show(Status::SHOW_DND);
+      }
+      else if (show->BodyText() == "chat") {
+        s.set_show(Status::SHOW_CHAT);
+      }
+      else {
+        s.set_show(Status::SHOW_ONLINE);
+      }
+    }
+
+    const XmlElement * caps = stanza->FirstNamed(QN_CAPS_C);
+    if (caps != NULL) {
+      std::string node = caps->Attr(QN_NODE);
+      std::string ver = caps->Attr(QN_VER);
+      std::string exts = caps->Attr(QN_EXT);
+
+      s.set_know_capabilities(true);
+      std::string capability;
+      std::stringstream ss(exts);
+      while (ss >> capability) {
+        s.AddCapability(capability);
+      }
+
+      if (node == GOOGLE_CLIENT_NODE) {
+        s.set_is_google_client(true);
+        s.set_version(ver);
+      }
+    }
+
+    const XmlElement* delay = stanza->FirstNamed(kQnDelayX);
+    if (delay != NULL) {
+      // Ideally we would parse this according to the Psuedo ISO-8601 rules
+      // that are laid out in JEP-0082:
+      // http://www.jabber.org/jeps/jep-0082.html
+      std::string stamp = delay->Attr(kQnStamp);
+      s.set_sent_time(stamp);
+    }
+
+    const XmlElement *nick = stanza->FirstNamed(kQnNickname);
+    if (nick) {
+      std::string user_nick = nick->BodyText();
+      s.set_user_nick(user_nick);
+    }
+
+    const XmlElement *plugin = stanza->FirstNamed(QN_PLUGIN);
+    if (plugin) {
+      const XmlElement *api_cap = plugin->FirstNamed(QN_CAPABILITY);
+      if (api_cap) {
+        const std::string &api_capability = api_cap->BodyText();
+        s.set_api_capability(api_capability);
+      }
+      const XmlElement *api_msg = plugin->FirstNamed(QN_DATA);
+      if (api_msg) {
+        const std::string &api_message = api_msg->BodyText();
+        s.set_api_message(api_message);
+      }
+    }
+
+    const XmlElement* data_x = stanza->FirstNamed(QN_MUC_USER_X);
+    if (data_x != NULL) {
+      const XmlElement* item = data_x->FirstNamed(QN_MUC_USER_ITEM);
+      if (item != NULL) {
+        s.set_muc_role(item->Attr(QN_ROLE));
+      }
+    }
+
+    SignalStatusUpdate(s);
+  }
+
+  return STATE_START;
+}
+
+
+}
diff --git a/talk/examples/plus/presencepushtask.h b/talk/examples/plus/presencepushtask.h
new file mode 100644
index 0000000..b9c8038
--- /dev/null
+++ b/talk/examples/plus/presencepushtask.h
@@ -0,0 +1,53 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#ifndef _PRESENCEPUSHTASK_H_
+#define _PRESENCEPUSHTASK_H_
+
+#include "talk/xmpp/xmppengine.h"
+#include "talk/xmpp/xmpptask.h"
+#include "talk/base/sigslot.h"
+#include "talk/app/status.h"
+
+namespace buzz {
+
+class PresencePushTask : public XmppTask {
+
+public:
+  PresencePushTask(Task * parent) : XmppTask(parent, XmppEngine::HL_TYPE) {}
+  virtual int ProcessStart();
+  sigslot::signal1<const Status &>SignalStatusUpdate;
+  sigslot::signal1<const XmlElement &> SignalStatusError;
+
+protected:
+  virtual bool HandleStanza(const XmlElement * stanza);
+};
+
+  
+}
+
+#endif
diff --git a/talk/examples/plus/rostertask.cc b/talk/examples/plus/rostertask.cc
new file mode 100644
index 0000000..1344d08
--- /dev/null
+++ b/talk/examples/plus/rostertask.cc
@@ -0,0 +1,218 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#include "rostertask.h"
+#include "talk/xmpp/constants.h"
+#include "talk/base/stream.h"
+
+#undef WIN32
+#ifdef WIN32
+#include "talk/app/win32/offlineroster.h"
+#endif
+
+namespace buzz {
+
+class RosterTask::RosterGetTask : public XmppTask {
+public:
+  RosterGetTask(Task * parent) : XmppTask(parent, XmppEngine::HL_SINGLE),
+    done_(false) {}
+
+  virtual int ProcessStart();
+  virtual int ProcessResponse();
+
+protected:
+  virtual bool HandleStanza(const XmlElement * stanza);
+
+  bool done_;
+};
+
+//==============================================================================
+// RosterTask
+//==============================================================================
+void RosterTask::RefreshRosterNow() {
+  RosterGetTask* get_task = new RosterGetTask(this);
+  ResumeTimeout();
+  get_task->Start();
+}
+
+void RosterTask::TranslateItems(const XmlElement * rosterQueryResult) {
+#if defined(FEATURE_ENABLE_PSTN)
+#ifdef WIN32
+  // We build up a list of contacts which have had information persisted offline.
+  // we'll remove items from this list if we get a buzz::SUBSCRIBE_REMOVE
+  // subscription.  After updating all the items from the server, we'll then
+  // update (and merge) any roster items left in our map of offline items
+  XmlElement *el_local = OfflineRoster::RetrieveOfflineRoster(GetClient()->jid());
+  std::map<buzz::Jid, RosterItem> jid_to_item;
+  if (el_local) {
+    for (XmlElement *el_item = el_local->FirstNamed(QN_ROSTER_ITEM);
+         el_item != NULL;
+         el_item = el_item->NextNamed(QN_ROSTER_ITEM)) {
+      RosterItem roster_item;
+      roster_item.FromXml(el_item);
+
+      jid_to_item[roster_item.jid()] = roster_item;
+    }
+  }
+#endif // WIN32
+#endif // FEATURE_ENABLE_PSTN
+
+  const XmlElement * xml_item;
+  for (xml_item = rosterQueryResult->FirstNamed(QN_ROSTER_ITEM);
+       xml_item != NULL; xml_item = xml_item->NextNamed(QN_ROSTER_ITEM)) {
+    RosterItem roster_item;
+    roster_item.FromXml(xml_item);
+
+    if (roster_item.subscription() == buzz::SUBSCRIBE_REMOVE) {
+      SignalRosterItemRemoved(roster_item);
+
+#if defined(FEATURE_ENABLE_PSTN)
+#ifdef WIN32
+      std::map<buzz::Jid, RosterItem>::iterator it =
+        jid_to_item.find(roster_item.jid());
+
+      if (it != jid_to_item.end())
+        jid_to_item.erase(it);
+#endif
+#endif
+    } else {
+      SignalRosterItemUpdated(roster_item, false);
+    }
+  }
+
+#if defined(FEATURE_ENABLE_PSTN)
+#ifdef WIN32
+  for (std::map<buzz::Jid, RosterItem>::iterator it = jid_to_item.begin();
+       it != jid_to_item.end(); ++it) {
+    SignalRosterItemUpdated(it->second, true);
+  }
+#endif
+#endif
+}
+
+int RosterTask::ProcessStart() {
+  const XmlElement * stanza = NextStanza();
+  if (stanza == NULL)
+    return STATE_BLOCKED;
+
+  if (stanza->Name() == QN_IQ) {
+    SuspendTimeout();
+    bool result = (stanza->Attr(QN_TYPE) == STR_RESULT);
+    if (result)
+      SignalRosterRefreshStarted();
+
+    TranslateItems(stanza->FirstNamed(QN_ROSTER_QUERY));
+
+    if (result)
+      SignalRosterRefreshFinished();
+  } else if (stanza->Name() == QN_PRESENCE) {
+    Jid jid(stanza->Attr(QN_FROM));
+    std::string type = stanza->Attr(QN_TYPE);
+    if (type == "subscribe")
+      SignalSubscribe(jid);
+    else if (type == "unsubscribe")
+      SignalUnsubscribe(jid);
+    else if (type == "subscribed")
+      SignalSubscribed(jid);
+    else if (type == "unsubscribed")
+      SignalUnsubscribed(jid);
+  }
+
+  return STATE_START;
+}
+
+bool RosterTask::HandleStanza(const XmlElement * stanza) {
+  if (!MatchRequestIq(stanza, STR_SET, QN_ROSTER_QUERY)) {
+    // Not a roster IQ.  Look for a presence instead
+    if (stanza->Name() != QN_PRESENCE)
+      return false;
+    if (!stanza->HasAttr(QN_TYPE))
+      return false;
+    std::string type = stanza->Attr(QN_TYPE);
+    if (type == "subscribe" || type == "unsubscribe" ||
+        type == "subscribed" || type == "unsubscribed") {
+      QueueStanza(stanza);
+      return true;
+    }
+    return false;
+  }
+
+  // only respect roster push from the server
+  Jid from(stanza->Attr(QN_FROM));
+  if (from != JID_EMPTY &&
+      !from.BareEquals(GetClient()->jid()) &&
+      from != Jid(GetClient()->jid().domain()))
+    return false;
+
+  XmlElement * result = MakeIqResult(stanza);
+  result->AddElement(new XmlElement(QN_ROSTER_QUERY, true));
+  SendStanza(result);
+
+  QueueStanza(stanza);
+  return true;
+}
+
+
+//==============================================================================
+// RosterTask::RosterGetTask
+//==============================================================================
+int RosterTask::RosterGetTask::ProcessStart() {
+  talk_base::scoped_ptr<XmlElement> get(MakeIq(STR_GET, JID_EMPTY, task_id()));
+  get->AddElement(new XmlElement(QN_ROSTER_QUERY, true));
+  get->AddAttr(QN_XMLNS_GR, NS_GR, 1);
+  get->AddAttr(QN_GR_EXT, "2", 1);
+  get->AddAttr(QN_GR_INCLUDE, "all", 1);
+  if (SendStanza(get.get()) != XMPP_RETURN_OK) {
+    return STATE_ERROR;
+  }
+  return STATE_RESPONSE;
+}
+
+int RosterTask::RosterGetTask::ProcessResponse() {
+  if (done_)
+    return STATE_DONE;
+  return STATE_BLOCKED;
+}
+
+bool RosterTask::RosterGetTask::HandleStanza(const XmlElement * stanza) {
+  if (!MatchResponseIq(stanza, JID_EMPTY, task_id()))
+    return false;
+
+  if (stanza->Attr(QN_TYPE) != STR_RESULT)
+    return false;
+
+  // Queue the stanza with the parent so these don't get handled out of order
+  RosterTask* parent = static_cast<RosterTask*>(GetParent());
+  parent->QueueStanza(stanza);
+
+  // Wake ourselves so we can go into the done state
+  done_ = true;
+  Wake();
+  return true;
+}
+
+}
diff --git a/talk/examples/plus/rostertask.h b/talk/examples/plus/rostertask.h
new file mode 100644
index 0000000..beab1f9
--- /dev/null
+++ b/talk/examples/plus/rostertask.h
@@ -0,0 +1,72 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#ifndef _PHONE_CLIENT_ROSTERTASK_H_
+#define _PHONE_CLIENT_ROSTERTASK_H_
+
+#include "talk/xmpp/xmppclient.h"
+#include "talk/xmpp/xmpptask.h"
+#include "talk/app/rosteritem.h"
+#include "talk/base/sigslot.h"
+
+namespace buzz {
+
+class RosterTask : public XmppTask {
+public:
+  RosterTask(Task * parent) :
+    XmppTask(parent, XmppEngine::HL_TYPE) {}
+
+  // Roster items removed or updated.  This can come from a push or a get
+  sigslot::signal2<const RosterItem &, bool> SignalRosterItemUpdated;
+  sigslot::signal1<const RosterItem &> SignalRosterItemRemoved;
+
+  // Subscription messages
+  sigslot::signal1<const Jid &> SignalSubscribe;
+  sigslot::signal1<const Jid &> SignalUnsubscribe;
+  sigslot::signal1<const Jid &> SignalSubscribed;
+  sigslot::signal1<const Jid &> SignalUnsubscribed;
+
+  // Roster get
+  void RefreshRosterNow();
+  sigslot::signal0<> SignalRosterRefreshStarted;
+  sigslot::signal0<> SignalRosterRefreshFinished;
+
+  virtual int ProcessStart();
+
+protected:
+  void TranslateItems(const XmlElement *rosterQueryResult);
+
+  virtual bool HandleStanza(const XmlElement * stanza);
+
+  // Inner class for doing the roster get
+  class RosterGetTask;
+  friend class RosterGetTask;
+};
+
+}
+
+#endif // _PHONE_CLIENT_ROSTERTASK_H_
diff --git a/talk/examples/plus/testutil/libjingleplus_main.cc b/talk/examples/plus/testutil/libjingleplus_main.cc
new file mode 100644
index 0000000..b5a0686
--- /dev/null
+++ b/talk/examples/plus/testutil/libjingleplus_main.cc
@@ -0,0 +1,119 @@
+/*
+ * libjingle
+ * Copyright 2006, 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.
+ */
+
+#include <iostream>
+#include <string>
+
+#include "talk/base/thread.h"
+#include "talk/libjingle-plus/libjingleplus.h"
+#include "talk/libjingle-plus/testutil/libjingleplus_test_notifier.h"
+
+#if defined(_MSC_VER) && (_MSC_VER < 1400)
+void __cdecl std::_Throw(const std::exception &) {}
+std::_Prhand std::_Raise_handler =0;
+#endif
+
+
+void SetConsoleEcho(bool on) {
+#ifdef WIN32
+  HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
+  if ((hIn == INVALID_HANDLE_VALUE) || (hIn == NULL))
+    return;
+
+  DWORD mode;
+  if (!GetConsoleMode(hIn, &mode))
+    return;
+
+  if (on) {
+    mode = mode | ENABLE_ECHO_INPUT;
+  } else {
+    mode = mode & ~ENABLE_ECHO_INPUT;
+  }
+
+  SetConsoleMode(hIn, mode);
+#else
+  if (on)
+    system("stty echo");
+  else
+    system("stty -echo");
+#endif
+}
+
+int main (int argc, char **argv)
+{
+  std::string username;
+  std::string password;
+
+  bool gaia = false;
+  
+  for (int i = 1; i < argc; i++) {
+    if (!strcmp(argv[i], "-gaia"))
+      gaia = true;
+  }
+ 
+  std::cout << "Username: ";
+  std::cin >> username;
+  std::cout << (gaia ? "Gaia cookie: " : "Password: ");
+  SetConsoleEcho(false);
+  std::cin >> password;
+  SetConsoleEcho(true);
+
+  // Create a LibjinglePlus object and give it the notifier interface
+  LibjinglePlus ljp(new Notifier);
+  
+  // Login
+  ljp.Login(username, password, "talk.google.com", false, gaia);
+  
+  buzz::Status s;
+  s.set_available(true);
+  s.set_show(buzz::Status::SHOW_ONLINE);
+  s.set_status("I'm online.");
+  
+  buzz::XmppMessage m;
+  m.set_to(buzz::Jid(username + "@gmail.com"));
+  m.set_body("What's up?");
+  
+  // Typically, you would wait for WakeupMainThread to be called, and then call
+  // DoCallbacks. Because I have nothing else to do on the main thread, I'm just going
+  // to do a few things after 10 seconds and then poll every 2ms.
+  Sleep(10000);
+  //  ljp.DoCallbacks();
+  ljp.SendPresence(s);
+  ljp.SendXmppMessage(m);
+
+#ifdef WIN32  
+  MSG msg;
+  while (GetMessage(&msg, NULL, 0, 0)) {
+    DispatchMessage(&msg);
+  }
+#else
+  for (;;) {
+    ljp.DoCallbacks();
+    Sleep(2);
+  }
+#endif
+}
diff --git a/talk/examples/plus/testutil/libjingleplus_test_notifier.h b/talk/examples/plus/testutil/libjingleplus_test_notifier.h
new file mode 100644
index 0000000..3a1af55
--- /dev/null
+++ b/talk/examples/plus/testutil/libjingleplus_test_notifier.h
@@ -0,0 +1,101 @@
+/*
+ * libjingle
+ * Copyright 2006, 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.
+ */
+
+#include <iostream>
+#include <string>
+
+#include "talk/libjingle-plus/libjingleplus.h"
+
+class Notifier : virtual public LibjinglePlusNotify {
+  virtual void OnStateChange(buzz::XmppEngine::State state) {
+    std::cout << "State change: " << state << std::endl;
+  }
+
+  virtual void OnSocketClose(int error_code) {
+    std::cout << "Socket close: " << error_code << std::endl;
+  }
+
+  virtual void OnXmppOutput(const std::string &output) {
+    std::cout << ">>>>>>>>" << std::endl << output << std::endl << ">>>>>>>>" << std::endl;
+  }
+  
+  virtual void OnXmppInput(const std::string &input) {
+    std::cout << "<<<<<<<<" << std::endl << input << std::endl << "<<<<<<<<" << std::endl;
+  } 
+
+  
+  virtual void OnStatusUpdate(const buzz::Status &status) {
+    std::string from = status.jid().Str();
+    std::cout  << from  << " - " << status.status() << std::endl;
+  }
+
+  virtual void OnStatusError(const buzz::XmlElement &stanza) {
+  }
+  
+  virtual void OnIqDone(bool success, const buzz::XmlElement &stanza) {
+  }
+
+  virtual void OnMessage(const buzz::XmppMessage &m) {
+    if (m.body() != "")
+      std::cout << m.from().Str() << ": " << m.body() << std::endl;
+  }
+
+  void OnRosterItemUpdated(const buzz::RosterItem &ri) {
+    std::cout << "Roster item: " << ri.jid().Str() << std::endl;
+  }
+  
+  virtual void OnRosterItemRemoved(const buzz::RosterItem &ri) {
+    std::cout << "Roster item removed: " << ri.jid().Str() << std::endl;
+  }
+
+  virtual void OnRosterSubscribe(const buzz::Jid& jid) {
+    std::cout << "Subscribing: " << jid.Str() << std::endl;
+  }
+
+  virtual void OnRosterUnsubscribe(const buzz::Jid &jid) {
+    std::cout << "Unsubscribing: " <<jid.Str() << std::endl;
+  }
+  
+  virtual void OnRosterSubscribed(const buzz::Jid &jid) {
+    std::cout << "Subscribed: " << jid.Str() << std::endl;
+  }
+  
+  virtual void OnRosterUnsubscribed(const buzz::Jid &jid) {
+    std::cout << "Unsubscribed: " << jid.Str() << std::endl;
+  }
+
+  virtual void OnRosterRefreshStarted() {
+    std::cout << "Refreshing roster." << std::endl;
+  }
+  
+  virtual void OnRosterRefreshFinished() {
+    std::cout << "Roster refreshed." << std::endl;
+  }
+
+  virtual void WakeupMainThread() {
+  }
+};
diff --git a/talk/examples/plus/testutil/libjingleplus_unittest.cc b/talk/examples/plus/testutil/libjingleplus_unittest.cc
new file mode 100644
index 0000000..d7f6325
--- /dev/null
+++ b/talk/examples/plus/testutil/libjingleplus_unittest.cc
@@ -0,0 +1,52 @@
+/*
+ * libjingle
+ * Copyright 2006, 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.
+ */
+
+#include "testing/base/gunit.h"
+#include "talk/libjingle-plus/libjingleplus.h"
+#include "talk/libjingle-plus/testutil/libjingleplus_test_notifier.h"
+
+#if defined(_MSC_VER) && (_MSC_VER < 1400)
+void __cdecl std::_Throw(const std::exception &) {}
+std::_Prhand std::_Raise_handler =0;
+#endif
+
+namespace talk_base {
+
+TEST(LibjingleTest, ConstructDestruct) {
+  for (int i = 0; i < 5; ++i) {
+    LibjinglePlus *libjingleplus = new LibjinglePlus(new Notifier);
+    libjingleplus->Login("eaterleaver0", "Buzzt3st", "talk.google.com", false, false);
+
+    delete libjingleplus;
+  }
+}
+}
+
+int main(int argc, char** argv) {
+  testing::ParseGUnitFlags(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/talk/libjingle.scons b/talk/libjingle.scons
index 8e40bbd..1904550 100644
--- a/talk/libjingle.scons
+++ b/talk/libjingle.scons
@@ -74,6 +74,7 @@
              lin_srcs = [
                "base/latebindingsymboltable.cc",
                "base/linux.cc",
+               "base/libdbusglibsymboltable.cc",
                "session/phone/libudevsymboltable.cc",
                "session/phone/linuxdevicemanager.cc",
                "session/phone/v4llookup.cc",
@@ -96,9 +97,13 @@
              },
              mac_srcs = [
                "base/macasyncsocket.cc",
+               "base/maccocoasocketserver.mm",
+               "base/maccocoathreadhelper.mm",
                "base/macconversion.cc",
                "base/macsocketserver.cc",
                "base/macutils.cc",
+               "base/macwindowpicker.cc",
+               "base/scoped_autorelease_pool.mm",
                "session/phone/carbonvideorenderer.cc",
                "session/phone/macdevicemanager.cc",
                "session/phone/macdevicemanagermm.mm",
@@ -162,6 +167,7 @@
                "base/optionsfile.cc",
                "base/pathutils.cc",
                "base/physicalsocketserver.cc",
+               "base/posix.cc",
                "base/proxydetect.cc",
                "base/proxyinfo.cc",
                "base/proxyserver.cc",
@@ -187,6 +193,7 @@
                "base/thread.cc",
                "base/timeutils.cc",
                "base/timing.cc",
+               "base/transformadapter.cc",
                "base/urlencode.cc",
                "base/versionparsing.cc",
                "base/virtualsocketserver.cc",
@@ -215,6 +222,7 @@
                "p2p/base/transportchannelproxy.cc",
                "p2p/base/udpport.cc",
                "p2p/client/basicportallocator.cc",
+               "p2p/client/connectivitychecker.cc",
                "p2p/client/httpportallocator.cc",
                "p2p/client/socketmonitor.cc",
                "session/tunnel/pseudotcpchannel.cc",
@@ -242,6 +250,7 @@
                "session/phone/soundclip.cc",
                "session/phone/srtpfilter.cc",
                "session/phone/ssrcmuxfilter.cc",
+               "session/phone/videoadapter.cc",
                "session/phone/videocapturer.cc",
                "session/phone/videocommon.cc",
                "session/phone/videoframe.cc",
@@ -262,9 +271,12 @@
                "xmpp/hangoutpubsubclient.cc",
                "xmpp/iqtask.cc",
                "xmpp/jid.cc",
+               "xmpp/moduleimpl.cc",
                "xmpp/mucroomconfigtask.cc",
+               "xmpp/mucroomhistorytask.cc",
                "xmpp/mucroomlookuptask.cc",
                "xmpp/pubsubclient.cc",
+               "xmpp/pubsub_task.cc",
                "xmpp/pubsubtasks.cc",
                "xmpp/receivetask.cc",
                "xmpp/saslmechanism.cc",
@@ -283,13 +295,16 @@
                "third_party/openssl/include",
              ],
              win_srcs = [
+               "base/diskcache_win32.cc",
                "base/schanneladapter.cc",
                "base/win32.cc",
+               "base/win32regkey.cc",
                "base/win32filesystem.cc",
                "base/win32securityerrors.cc",
                "base/win32socketserver.cc",
                "base/win32socketinit.cc",
                "base/win32window.cc",
+               "base/win32windowpicker.cc",
                "base/winfirewall.cc",
                "base/winping.cc",
                "session/phone/win32devicemanager.cc",
@@ -299,7 +314,11 @@
                "-Wno-deprecated-declarations",
              ],
              extra_srcs = [
+               "base/dbus.cc",
                "base/json.cc",
+               "base/natserver_main.cc",
+               "xmpp/chatroommoduleimpl.cc",
+               "xmpp/rostermoduleimpl.cc",
              ],
 )
 talk.Library(env, name = "xmpphelp",
@@ -307,6 +326,7 @@
                "jingle",
              ],
              srcs = [
+               "examples/login/jingleinfotask.cc",
                "examples/login/xmppauth.cc",
                "examples/login/xmpppump.cc",
                "examples/login/xmppsocket.cc",
@@ -379,9 +399,6 @@
            "crypto",
            "ssl",
          ],
-         cppdefines = [
-           "FEATURE_ENABLE_VOICEMAIL",
-         ],
          lin_libs = [
            "videorenderer",
          ],
@@ -389,14 +406,12 @@
            "examples/call/call_main.cc",
            "examples/call/callclient.cc",
            "examples/call/console.cc",
-           "examples/call/discoitemsquerytask.cc",
            "examples/call/friendinvitesendtask.cc",
            "examples/call/mediaenginefactory.cc",
            "examples/call/mucinviterecvtask.cc",
            "examples/call/mucinvitesendtask.cc",
            "examples/call/presenceouttask.cc",
            "examples/call/presencepushtask.cc",
-           "examples/call/voicemailjidrequester.cc",
          ],
          libs = [
            "jingle",
@@ -422,14 +437,30 @@
          ],
 )
 talk.Unittest(env, name = "base",
-              libs = [
-                "jingle",
+              lin_srcs = [
+                "base/latebindingsymboltable_unittest.cc",
+                "base/linux_unittest.cc",
+              ],
+              mac_srcs = [
+                "base/macsocketserver_unittest.cc",
+                "base/macutils_unittest.cc",
+                "base/macwindowpicker_unittest.cc",
+              ],
+              posix_srcs = [
+                "base/sslidentity_unittest.cc",
+                "base/sslstreamadapter_unittest.cc",
+              ],
+              cppdefines = [
+                "LIBJINGLE_UNITTEST",
+                "EXPAT_RELATIVE_PATH",
+                "SRTP_RELATIVE_PATH",
               ],
               srcs = [
                 "base/asynchttprequest_unittest.cc",
                 "base/atomicops_unittest.cc",
                 "base/autodetectproxy_unittest.cc",
                 "base/bandwidthsmoother_unittest.cc",
+                "base/base64_unittest.cc",
                 "base/buffer_unittest.cc",
                 "base/bytebuffer_unittest.cc",
                 "base/cpuid_unittest.cc",
@@ -450,6 +481,7 @@
                 "base/network_unittest.cc",
                 "base/optionsfile_unittest.cc",
                 "base/pathutils_unittest.cc",
+                "base/physicalsocketserver_unittest.cc",
                 "base/proxy_unittest.cc",
                 "base/proxydetect_unittest.cc",
                 "base/ratetracker_unittest.cc",
@@ -477,14 +509,20 @@
                 "third_party/srtp",
                 "third_party/gtest",
               ],
-              posix_srcs = [
-                "base/sslidentity_unittest.cc",
-                "base/sslstreamadapter_unittest.cc",
+              win_srcs = [
+                "base/win32_unittest.cc",
+                "base/win32regkey_unittest.cc",
+                "base/win32socketserver_unittest.cc",
+                "base/win32toolhelp_unittest.cc",
+                "base/win32window_unittest.cc",
+                "base/win32windowpicker_unittest.cc",
+                "base/winfirewall_unittest.cc",
               ],
-              cppdefines = [
-                "LIBJINGLE_UNITTEST",
-                "EXPAT_RELATIVE_PATH",
-                "SRTP_RELATIVE_PATH",
+              libs = [
+                "jingle",
+              ],
+              extra_srcs = [
+                "base/json_unittest.cc",
               ],
 )
 talk.Unittest(env, name = "p2p",
@@ -514,6 +552,7 @@
                 "p2p/base/stunrequest_unittest.cc",
                 "p2p/base/stunserver_unittest.cc",
                 "p2p/base/transport_unittest.cc",
+                "p2p/client/connectivitychecker_unittest.cc",
                 "p2p/client/portallocator_unittest.cc",
               ],
               includedirs = [
@@ -624,9 +663,14 @@
               ],
 )
 talk.Unittest(env, name = "xmpp",
-              libs = [
-                "jingle",
-                "expat",
+              mac_libs = [
+                "crypto",
+                "ssl",
+              ],
+              cppdefines = [
+                "LIBJINGLE_UNITTEST",
+                "EXPAT_RELATIVE_PATH",
+                "SRTP_RELATIVE_PATH",
               ],
               srcs = [
                 "xmpp/hangoutpubsubclient_unittest.cc",
@@ -640,19 +684,17 @@
                 "xmpp/xmpplogintask_unittest.cc",
                 "xmpp/xmppstanzaparser_unittest.cc",
               ],
-              mac_libs = [
-                "crypto",
-                "ssl",
-              ],
               includedirs = [
                 "third_party/gtest/include",
                 "third_party/expat-2.0.1/lib",
                 "third_party/srtp",
                 "third_party/gtest",
               ],
-              cppdefines = [
-                "LIBJINGLE_UNITTEST",
-                "EXPAT_RELATIVE_PATH",
-                "SRTP_RELATIVE_PATH",
+              libs = [
+                "jingle",
+                "expat",
+              ],
+              extra_srcs = [
+                "xmpp/chatroommodule_unittest.cc",
               ],
 )
diff --git a/talk/p2p/client/connectivitychecker.cc b/talk/p2p/client/connectivitychecker.cc
new file mode 100644
index 0000000..a5a36cc
--- /dev/null
+++ b/talk/p2p/client/connectivitychecker.cc
@@ -0,0 +1,511 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+
+#include <string>
+
+#include "talk/p2p/client/connectivitychecker.h"
+
+#include "talk/base/asynchttprequest.h"
+#include "talk/base/autodetectproxy.h"
+#include "talk/base/basicpacketsocketfactory.h"
+#include "talk/base/helpers.h"
+#include "talk/base/httpcommon.h"
+#include "talk/base/logging.h"
+#include "talk/base/proxydetect.h"
+#include "talk/base/thread.h"
+#include "talk/p2p/base/candidate.h"
+#include "talk/p2p/base/common.h"
+#include "talk/p2p/base/port.h"
+#include "talk/p2p/base/relayport.h"
+#include "talk/p2p/base/stunport.h"
+
+namespace cricket {
+
+static const char kSessionTypeVideo[] =
+    "http://www.google.com/session/video";
+static const char kSessionNameRtp[] = "rtp";
+
+static const char kDefaultStunHostname[] = "stun.l.google.com";
+static const int kDefaultStunPort = 19302;
+
+// Default maximum time in milliseconds we will wait for connections.
+static const uint32 kDefaultTimeoutMs = 3000;
+
+enum {
+  MSG_START = 1,
+  MSG_STOP = 2,
+  MSG_TIMEOUT = 3,
+  MSG_SIGNAL_RESULTS = 4
+};
+
+class TestHttpPortAllocator : public HttpPortAllocator {
+ public:
+  TestHttpPortAllocator(talk_base::NetworkManager* network_manager,
+                        const std::string& user_agent,
+                        const std::string& relay_token) :
+      HttpPortAllocator(network_manager, user_agent) {
+    SetRelayToken(relay_token);
+  }
+  PortAllocatorSession* CreateSession(
+      const std::string& name, const std::string& session_type) {
+    return new TestHttpPortAllocatorSession(this, name, session_type,
+                                            stun_hosts(), relay_hosts(),
+                                            relay_token(), user_agent());
+  }
+};
+
+void TestHttpPortAllocatorSession::ConfigReady(PortConfiguration* config) {
+  SignalConfigReady(config, proxy_);
+}
+
+void TestHttpPortAllocatorSession::OnRequestDone(
+    talk_base::SignalThread* data) {
+  talk_base::AsyncHttpRequest* request =
+      static_cast<talk_base::AsyncHttpRequest*>(data);
+  SignalRequestDone(request);
+
+  // For the https response, pass the result back to the port
+  // allocator to generate a config with relay addresses we can
+  // ping. For http, we are only interested in that we got a
+  // response.
+  if (request->port() == talk_base::HTTP_SECURE_PORT) {
+    HttpPortAllocatorSession::OnRequestDone(data);
+  }
+}
+
+ConnectivityChecker::ConnectivityChecker(
+    talk_base::Thread* worker,
+    const std::string& jid,
+    const std::string& session_id,
+    const std::string& user_agent,
+    const std::string& relay_token,
+    const std::string& connection)
+    : worker_(worker),
+      jid_(jid),
+      session_id_(session_id),
+      user_agent_(user_agent),
+      relay_token_(relay_token),
+      connection_(connection),
+      proxy_detect_(NULL),
+      timeout_ms_(kDefaultTimeoutMs),
+      stun_address_(kDefaultStunHostname, kDefaultStunPort) {
+}
+
+ConnectivityChecker::~ConnectivityChecker() {
+  Stop();
+  nics_.clear();
+}
+
+bool ConnectivityChecker::Initialize() {
+  network_manager_.reset(CreateNetworkManager());
+  socket_factory_.reset(CreateSocketFactory(worker_));
+  port_allocator_.reset(CreatePortAllocator(network_manager_.get(),
+                                            user_agent_, relay_token_));
+  return true;
+}
+
+void ConnectivityChecker::Start() {
+  main_ = talk_base::Thread::Current();
+  worker_->Post(this, MSG_START);
+}
+
+void ConnectivityChecker::Stop() {
+  worker_->Post(this, MSG_STOP);
+}
+
+void ConnectivityChecker::CleanUp() {
+  ASSERT(worker_ == talk_base::Thread::Current());
+  worker_->Clear(this, MSG_TIMEOUT);
+  if (proxy_detect_) {
+    proxy_detect_->Release();
+    proxy_detect_ = NULL;
+  }
+
+  for (uint32 i = 0; i < sessions_.size(); ++i) {
+    delete sessions_[i];
+  }
+  sessions_.clear();
+  for (uint32 i = 0; i < ports_.size(); ++i) {
+    delete ports_[i];
+  }
+  ports_.clear();
+}
+
+bool ConnectivityChecker::AddNic(const talk_base::IPAddress& ip,
+                                 const talk_base::SocketAddress& proxy_addr) {
+  NicMap::iterator i = nics_.find(NicId(ip, proxy_addr));
+  if (i != nics_.end()) {
+    // Already have it.
+    return false;
+  }
+  uint32 now = talk_base::Time();
+  NicInfo info;
+  info.ip = ip;
+  info.proxy_info = GetProxyInfo();
+  info.stun.start_time_ms = now;
+  nics_.insert(std::pair<NicId, NicInfo>(NicId(ip, proxy_addr), info));
+  return true;
+}
+
+void ConnectivityChecker::SetProxyInfo(const talk_base::ProxyInfo& proxy_info) {
+  port_allocator_->set_proxy(user_agent_, proxy_info);
+  AllocatePorts();
+}
+
+talk_base::ProxyInfo ConnectivityChecker::GetProxyInfo() const {
+  talk_base::ProxyInfo proxy_info;
+  if (proxy_detect_) {
+    proxy_info = proxy_detect_->proxy();
+  }
+  return proxy_info;
+}
+
+void ConnectivityChecker::CheckNetworks() {
+  network_manager_->SignalNetworksChanged.connect(
+      this, &ConnectivityChecker::OnNetworksChanged);
+  network_manager_->StartUpdating();
+}
+
+void ConnectivityChecker::OnMessage(talk_base::Message *msg) {
+  switch (msg->message_id) {
+    case MSG_START:
+      ASSERT(worker_ == talk_base::Thread::Current());
+      worker_->PostDelayed(timeout_ms_, this, MSG_TIMEOUT);
+      CheckNetworks();
+      break;
+    case MSG_STOP:
+      // We were stopped, just close down without signaling.
+      OnCheckDone(false);
+      break;
+    case MSG_TIMEOUT:
+      // Close down and signal results.
+      OnCheckDone(true);
+      break;
+    case MSG_SIGNAL_RESULTS:
+      ASSERT(main_ == talk_base::Thread::Current());
+      SignalCheckDone(this);
+      break;
+    default:
+      LOG(LS_ERROR) << "Unknown message: " << msg->message_id;
+  }
+}
+
+void ConnectivityChecker::OnCheckDone(bool signal_results) {
+  // Clean up memory allocated by the worker thread.
+  CleanUp();
+
+  if (signal_results) {
+    main_->Post(this, MSG_SIGNAL_RESULTS);
+  }
+}
+
+void ConnectivityChecker::OnProxyDetect(talk_base::SignalThread* thread) {
+  ASSERT(worker_ == talk_base::Thread::Current());
+  if (proxy_detect_->proxy().type != talk_base::PROXY_NONE) {
+    SetProxyInfo(proxy_detect_->proxy());
+  }
+}
+
+void ConnectivityChecker::OnRequestDone(talk_base::AsyncHttpRequest* request) {
+  ASSERT(worker_ == talk_base::Thread::Current());
+  // Since we don't know what nic were actually used for the http request,
+  // for now, just use the first one.
+  std::vector<talk_base::Network*> networks;
+  network_manager_->GetNetworks(&networks);
+  if (networks.empty()) {
+    LOG(LS_ERROR) << "No networks while registering http start.";
+    return;
+  }
+  talk_base::ProxyInfo proxy_info = request->proxy();
+  NicMap::iterator i = nics_.find(NicId(networks[0]->ip(), proxy_info.address));
+  if (i != nics_.end()) {
+    int port = request->port();
+    uint32 now = talk_base::Time();
+    NicInfo* nic_info = &i->second;
+    if (port == talk_base::HTTP_DEFAULT_PORT) {
+      nic_info->http.rtt = now - nic_info->http.start_time_ms;
+    } else if (port == talk_base::HTTP_SECURE_PORT) {
+      nic_info->https.rtt = now - nic_info->https.start_time_ms;
+    } else {
+      LOG(LS_ERROR) << "Got response with unknown port: " << port;
+    }
+  } else {
+    LOG(LS_ERROR) << "No nic info found while receiving response.";
+  }
+}
+
+void ConnectivityChecker::OnConfigReady(
+    const PortConfiguration* config,
+    const talk_base::ProxyInfo& proxy_info) {
+  ASSERT(worker_ == talk_base::Thread::Current());
+  CreateRelayPorts(config, proxy_info);
+}
+
+void ConnectivityChecker::OnRelayAddressReady(Port* port) {
+  ASSERT(worker_ == talk_base::Thread::Current());
+  RelayPort* relay_port = reinterpret_cast<RelayPort*>(port);
+  const ProtocolAddress* address = relay_port->ServerAddress(0);
+  talk_base::IPAddress ip = port->network()->ip();
+  NicMap::iterator i = nics_.find(NicId(ip, port->proxy().address));
+  if (i != nics_.end()) {
+    // We have it already, add the new information.
+    NicInfo* nic_info = &i->second;
+    ConnectInfo* connect_info = NULL;
+    if (address) {
+      switch (address->proto) {
+        case PROTO_UDP:
+          connect_info = &nic_info->udp;
+          break;
+        case PROTO_TCP:
+          connect_info = &nic_info->tcp;
+          break;
+        case PROTO_SSLTCP:
+          connect_info = &nic_info->ssltcp;
+          break;
+        default:
+          LOG(LS_ERROR) << " relay address with bad protocol added";
+      }
+      if (connect_info) {
+        connect_info->rtt =
+            talk_base::TimeSince(connect_info->start_time_ms);
+      }
+    }
+  } else {
+    LOG(LS_ERROR) << " got relay address for non-existing nic";
+  }
+}
+
+void ConnectivityChecker::OnStunAddressReady(Port* port) {
+  ASSERT(worker_ == talk_base::Thread::Current());
+  const std::vector<Candidate> candidates = port->candidates();
+  Candidate c = candidates[0];
+  talk_base::IPAddress ip = port->network()->ip();
+  NicMap::iterator i = nics_.find(NicId(ip, port->proxy().address));
+  if (i != nics_.end()) {
+    // We have it already, add the new information.
+    uint32 now = talk_base::Time();
+    NicInfo* nic_info = &i->second;
+    nic_info->external_address = c.address();
+    nic_info->stun_server_address = static_cast<StunPort*>(port)->server_addr();
+    nic_info->stun.rtt = now - nic_info->stun.start_time_ms;
+  } else {
+    LOG(LS_ERROR) << "Got stun address for non-existing nic";
+  }
+}
+
+void ConnectivityChecker::OnStunAddressError(Port* port) {
+  ASSERT(worker_ == talk_base::Thread::Current());
+  LOG(LS_ERROR) << "Stun address error.";
+  talk_base::IPAddress ip = port->network()->ip();
+  NicMap::iterator i = nics_.find(NicId(ip, port->proxy().address));
+  if (i != nics_.end()) {
+    // We have it already, add the new information.
+    NicInfo* nic_info = &i->second;
+    nic_info->stun_server_address = static_cast<StunPort*>(port)->server_addr();
+  }
+}
+
+void ConnectivityChecker::OnRelayAddressError(Port* port) {
+  ASSERT(worker_ == talk_base::Thread::Current());
+  LOG(LS_ERROR) << "Relay address error.";
+}
+
+void ConnectivityChecker::OnNetworksChanged() {
+  ASSERT(worker_ == talk_base::Thread::Current());
+  std::vector<talk_base::Network*> networks;
+  network_manager_->GetNetworks(&networks);
+  if (networks.empty()) {
+    LOG(LS_ERROR) << "Machine has no networks; nothing to do";
+    return;
+  }
+  AllocatePorts();
+}
+
+HttpPortAllocator* ConnectivityChecker::CreatePortAllocator(
+    talk_base::NetworkManager* network_manager,
+    const std::string& user_agent,
+    const std::string& relay_token) {
+  return new TestHttpPortAllocator(network_manager, user_agent, relay_token);
+}
+
+StunPort* ConnectivityChecker::CreateStunPort(
+    const PortConfiguration* config, talk_base::Network* network) {
+  return StunPort::Create(worker_,
+                          socket_factory_.get(),
+                          network,
+                          network->ip(),
+                          0,
+                          0,
+                          config->stun_address);
+}
+
+RelayPort* ConnectivityChecker::CreateRelayPort(
+    const PortConfiguration* config, talk_base::Network* network) {
+  return RelayPort::Create(worker_,
+                           socket_factory_.get(),
+                           network,
+                           network->ip(),
+                           port_allocator_->min_port(),
+                           port_allocator_->max_port(),
+                           config->username,
+                           config->password,
+                           config->magic_cookie);
+}
+
+void ConnectivityChecker::CreateRelayPorts(
+    const PortConfiguration* config,
+    const talk_base::ProxyInfo& proxy_info) {
+  PortConfiguration::RelayList::const_iterator relay;
+  std::vector<talk_base::Network*> networks;
+  network_manager_->GetNetworks(&networks);
+  if (networks.empty()) {
+    LOG(LS_ERROR) << "Machine has no networks; no relay ports created.";
+    return;
+  }
+  for (relay = config->relays.begin();
+       relay != config->relays.end(); ++relay) {
+    for (uint32 i = 0; i < networks.size(); ++i) {
+      NicMap::iterator iter = nics_.find(NicId(networks[i]->ip(),
+                                               proxy_info.address));
+      if (iter != nics_.end()) {
+        // TODO: Now setting the same start time for all protocols.
+        // This might affect accuracy, but since we are mainly looking for
+        // connect failures or number that stick out, this is good enough.
+        uint32 now = talk_base::Time();
+        NicInfo* nic_info = &iter->second;
+        nic_info->udp.start_time_ms = now;
+        nic_info->tcp.start_time_ms = now;
+        nic_info->ssltcp.start_time_ms = now;
+
+        // Add the addresses of this protocol.
+        PortConfiguration::PortList::const_iterator relay_port;
+        for (relay_port = relay->ports.begin();
+             relay_port != relay->ports.end();
+             ++relay_port) {
+          RelayPort* port = CreateRelayPort(config, networks[i]);
+          port->AddServerAddress(*relay_port);
+          port->AddExternalAddress(*relay_port);
+
+          nic_info->media_server_address = port->ServerAddress(0)->address;
+
+          // Listen to network events.
+          port->SignalAddressReady.connect(
+              this, &ConnectivityChecker::OnRelayAddressReady);
+          port->SignalAddressError.connect(
+              this, &ConnectivityChecker::OnRelayAddressError);
+
+          port->set_proxy(user_agent_, proxy_info);
+
+          // Start fetching an address for this port.
+          port->PrepareAddress();
+          ports_.push_back(port);
+        }
+      } else {
+        LOG(LS_ERROR) << "Failed to find nic info when creating relay ports.";
+      }
+    }
+  }
+}
+
+void ConnectivityChecker::AllocatePorts() {
+  PortConfiguration config(stun_address_,
+                           talk_base::CreateRandomString(16),
+                           talk_base::CreateRandomString(16),
+                           "");
+  std::vector<talk_base::Network*> networks;
+  network_manager_->GetNetworks(&networks);
+  if (networks.empty()) {
+    LOG(LS_ERROR) << "Machine has no networks; no ports will be allocated";
+    return;
+  }
+  talk_base::ProxyInfo proxy_info = GetProxyInfo();
+  bool allocateRelayPorts = false;
+  for (uint32 i = 0; i < networks.size(); ++i) {
+    if (AddNic(networks[i]->ip(), proxy_info.address)) {
+      Port* port = CreateStunPort(&config, networks[i]);
+
+      // Listen to network events.
+      port->SignalAddressReady.connect(
+          this, &ConnectivityChecker::OnStunAddressReady);
+      port->SignalAddressError.connect(
+          this, &ConnectivityChecker::OnStunAddressError);
+
+      port->set_proxy(user_agent_, proxy_info);
+      port->PrepareAddress();
+      ports_.push_back(port);
+      allocateRelayPorts = true;
+    }
+  }
+
+  // If any new ip/proxy combinations were added, send a relay allocate.
+  if (allocateRelayPorts) {
+    AllocateRelayPorts();
+  }
+
+  // Initiate proxy detection.
+  InitiateProxyDetection();
+}
+
+void ConnectivityChecker::InitiateProxyDetection() {
+  // Only start if we haven't been started before.
+  if (!proxy_detect_) {
+    proxy_detect_ = new talk_base::AutoDetectProxy(user_agent_);
+    talk_base::Url<char> host_url("/", "relay.google.com",
+                                  talk_base::HTTP_DEFAULT_PORT);
+    host_url.set_secure(true);
+    proxy_detect_->set_server_url(host_url.url());
+    proxy_detect_->SignalWorkDone.connect(
+        this, &ConnectivityChecker::OnProxyDetect);
+    proxy_detect_->Start();
+  }
+}
+
+void ConnectivityChecker::AllocateRelayPorts() {
+  // Currently we are using the 'default' nic for http(s) requests.
+  TestHttpPortAllocatorSession* allocator_session =
+      reinterpret_cast<TestHttpPortAllocatorSession*>(
+          port_allocator_->CreateSession(kSessionNameRtp, kSessionTypeVideo));
+  allocator_session->set_proxy(port_allocator_->proxy());
+  allocator_session->SignalConfigReady.connect(
+      this, &ConnectivityChecker::OnConfigReady);
+  allocator_session->SignalRequestDone.connect(
+      this, &ConnectivityChecker::OnRequestDone);
+
+  // Try both http and https.
+  RegisterHttpStart(talk_base::HTTP_SECURE_PORT);
+  allocator_session->SendSessionRequest("relay.l.google.com",
+                                        talk_base::HTTP_SECURE_PORT);
+  RegisterHttpStart(talk_base::HTTP_DEFAULT_PORT);
+  allocator_session->SendSessionRequest("relay.l.google.com",
+                                        talk_base::HTTP_DEFAULT_PORT);
+
+  sessions_.push_back(allocator_session);
+}
+
+void ConnectivityChecker::RegisterHttpStart(int port) {
+  // Since we don't know what nic were actually used for the http request,
+  // for now, just use the first one.
+  std::vector<talk_base::Network*> networks;
+  network_manager_->GetNetworks(&networks);
+  if (networks.empty()) {
+    LOG(LS_ERROR) << "No networks while registering http start.";
+    return;
+  }
+  talk_base::ProxyInfo proxy_info = GetProxyInfo();
+  NicMap::iterator i = nics_.find(NicId(networks[0]->ip(), proxy_info.address));
+  if (i != nics_.end()) {
+    uint32 now = talk_base::Time();
+    NicInfo* nic_info = &i->second;
+    if (port == talk_base::HTTP_DEFAULT_PORT) {
+      nic_info->http.start_time_ms = now;
+    } else if (port == talk_base::HTTP_SECURE_PORT) {
+      nic_info->https.start_time_ms = now;
+    } else {
+      LOG(LS_ERROR) << "Registering start time for unknown port: " << port;
+    }
+  } else {
+    LOG(LS_ERROR) << "Error, no nic info found while registering http start.";
+  }
+}
+
+}  // namespace talk_base
diff --git a/talk/p2p/client/connectivitychecker.h b/talk/p2p/client/connectivitychecker.h
new file mode 100644
index 0000000..18a1c7f
--- /dev/null
+++ b/talk/p2p/client/connectivitychecker.h
@@ -0,0 +1,260 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+
+#ifndef TALK_P2P_CLIENT_CONNECTIVITYCHECKER_H_
+#define TALK_P2P_CLIENT_CONNECTIVITYCHECKER_H_
+
+#include <map>
+#include <string>
+
+#include "talk/base/network.h"
+#include "talk/base/basicpacketsocketfactory.h"
+#include "talk/base/basictypes.h"
+#include "talk/base/messagehandler.h"
+#include "talk/base/proxyinfo.h"
+#include "talk/base/scoped_ptr.h"
+#include "talk/base/sigslot.h"
+#include "talk/base/socketaddress.h"
+#include "talk/p2p/client/httpportallocator.h"
+
+namespace talk_base {
+class AsyncHttpRequest;
+class AutoDetectProxy;
+class BasicPacketSocketFactory;
+class NetworkManager;
+class PacketSocketFactory;
+class SignalThread;
+class TestHttpPortAllocatorSession;
+class Thread;
+}
+
+namespace cricket {
+class HttpPortAllocator;
+class Port;
+class PortAllocatorSession;
+struct PortConfiguration;
+class RelayPort;
+class StunPort;
+
+// Contains details about a discovered firewall that are of interest
+// when debugging call failures.
+struct FirewallInfo {
+  std::string brand;
+  std::string model;
+
+  // TODO: List of current port mappings.
+};
+
+// Contains details about a specific connect attempt.
+struct ConnectInfo {
+  ConnectInfo()
+      : rtt(-1), error(0) {}
+  // Time when the connection was initiated. Needed for calculating
+  // the round trip time.
+  uint32 start_time_ms;
+  // Round trip time in milliseconds or -1 for failed connection.
+  int32 rtt;
+  // Error code representing low level errors like socket errors.
+  int error;
+};
+
+// Identifier for a network interface and proxy address pair.
+struct NicId {
+  NicId(const talk_base::IPAddress& ip,
+        const talk_base::SocketAddress& proxy_address)
+      : ip(ip),
+        proxy_address(proxy_address) {
+  }
+  talk_base::IPAddress ip;
+  talk_base::SocketAddress proxy_address;
+};
+
+// Comparator implementation identifying unique network interface and
+// proxy address pairs.
+class NicIdComparator {
+ public:
+  int compare(const NicId &first, const NicId &second) const {
+    if (first.ip == second.ip) {
+      // Compare proxy address.
+      if (first.proxy_address == second.proxy_address) {
+        return 0;
+      } else {
+        return first.proxy_address < second.proxy_address? -1 : 1;
+      }
+    }
+    return first.ip < second.ip ? -1 : 1;
+  }
+
+  bool operator()(const NicId &first, const NicId &second) const {
+    return (compare(first, second) < 0);
+  }
+};
+
+// Contains information of a network interface and proxy address pair.
+struct NicInfo {
+  NicInfo() {}
+  talk_base::IPAddress ip;
+  talk_base::ProxyInfo proxy_info;
+  talk_base::SocketAddress external_address;
+  talk_base::SocketAddress stun_server_address;
+  talk_base::SocketAddress media_server_address;
+  ConnectInfo stun;
+  ConnectInfo http;
+  ConnectInfo https;
+  ConnectInfo udp;
+  ConnectInfo tcp;
+  ConnectInfo ssltcp;
+  FirewallInfo firewall;
+};
+
+// Holds the result of the connectivity check.
+class NicMap : public std::map<NicId, NicInfo, NicIdComparator> {
+};
+
+class TestHttpPortAllocatorSession : public HttpPortAllocatorSession {
+ public:
+  TestHttpPortAllocatorSession(
+      HttpPortAllocator* allocator, const std::string &name,
+      const std::string& session_type,
+      const std::vector<talk_base::SocketAddress>& stun_hosts,
+      const std::vector<std::string>& relay_hosts,
+      const std::string& relay_token,
+      const std::string& user_agent)
+      : HttpPortAllocatorSession(allocator, name, session_type, stun_hosts,
+                                 relay_hosts, relay_token, user_agent) {
+  }
+  void set_proxy(const talk_base::ProxyInfo& proxy) {
+    proxy_ = proxy;
+  }
+
+  void ConfigReady(PortConfiguration* config);
+
+  void OnRequestDone(talk_base::SignalThread* data);
+
+  sigslot::signal2<const PortConfiguration*,
+                   const talk_base::ProxyInfo&> SignalConfigReady;
+  sigslot::signal1<talk_base::AsyncHttpRequest*> SignalRequestDone;
+
+ private:
+  talk_base::ProxyInfo proxy_;
+};
+
+// Runs a request/response check on all network interface and proxy
+// address combinations. The check is considered done either when all
+// checks has been successful or when the check times out.
+class ConnectivityChecker
+    : public talk_base::MessageHandler, public sigslot::has_slots<> {
+ public:
+  ConnectivityChecker(talk_base::Thread* worker,
+                      const std::string& jid,
+                      const std::string& session_id,
+                      const std::string& user_agent,
+                      const std::string& relay_token,
+                      const std::string& connection);
+  virtual ~ConnectivityChecker();
+
+  // Virtual for gMock.
+  virtual bool Initialize();
+  virtual void Start();
+  virtual void Stop();
+
+  // MessageHandler implementation.
+  virtual void OnMessage(talk_base::Message *msg);
+
+  const NicMap& GetResults() const {
+    return nics_;
+  }
+
+  void set_timeout_ms(uint32 timeout) {
+    timeout_ms_ = timeout;
+  }
+
+  void set_stun_address(const talk_base::SocketAddress& stun_address) {
+    stun_address_ = stun_address;
+  }
+
+  const std::string& connection() const {
+    return connection_;
+  }
+
+  talk_base::Thread* worker() {
+    return worker_;
+  }
+
+  const std::string& jid() const {
+    return jid_;
+  }
+
+  const std::string& session_id() const {
+    return session_id_;
+  }
+
+  // Context: Main Thread. Signalled when the connectivity check is complete.
+  sigslot::signal1<ConnectivityChecker*> SignalCheckDone;
+
+ protected:
+  // Can be overridden for test.
+  virtual talk_base::NetworkManager* CreateNetworkManager() {
+    return new talk_base::BasicNetworkManager();
+  }
+  virtual talk_base::BasicPacketSocketFactory* CreateSocketFactory(
+      talk_base::Thread* thread) {
+    return new talk_base::BasicPacketSocketFactory(thread);
+  }
+  virtual HttpPortAllocator* CreatePortAllocator(
+      talk_base::NetworkManager* network_manager,
+      const std::string& user_agent,
+      const std::string& relay_token);
+  virtual StunPort* CreateStunPort(const PortConfiguration* config,
+                                   talk_base::Network* network);
+  virtual RelayPort* CreateRelayPort(const PortConfiguration* config,
+                                     talk_base::Network* network);
+  virtual void InitiateProxyDetection();
+  virtual void SetProxyInfo(const talk_base::ProxyInfo& info);
+  virtual talk_base::ProxyInfo GetProxyInfo() const;
+
+ private:
+  bool AddNic(const talk_base::IPAddress& ip,
+              const talk_base::SocketAddress& proxy_address);
+  void AllocatePorts();
+  void AllocateRelayPorts();
+  void CheckNetworks();
+  void CreateRelayPorts(const PortConfiguration* config,
+                        const talk_base::ProxyInfo& proxy_info);
+
+  // Must be called by the worker thread.
+  void CleanUp();
+
+  void OnCheckDone(bool signal_results);
+  void OnRequestDone(talk_base::AsyncHttpRequest* request);
+  void OnRelayAddressReady(Port* port);
+  void OnStunAddressReady(Port* port);
+  void OnRelayAddressError(Port* port);
+  void OnStunAddressError(Port* port);
+  void OnNetworksChanged();
+  void OnProxyDetect(talk_base::SignalThread* thread);
+  void OnConfigReady(const PortConfiguration*,
+                     const talk_base::ProxyInfo& proxy);
+  void OnConfigWithProxyReady(const PortConfiguration*);
+  void RegisterHttpStart(int port);
+  talk_base::Thread* worker_;
+  std::string jid_;
+  std::string session_id_;
+  std::string user_agent_;
+  std::string relay_token_;
+  std::string connection_;
+  talk_base::AutoDetectProxy* proxy_detect_;
+  talk_base::scoped_ptr<talk_base::NetworkManager> network_manager_;
+  talk_base::scoped_ptr<talk_base::BasicPacketSocketFactory> socket_factory_;
+  talk_base::scoped_ptr<HttpPortAllocator> port_allocator_;
+  NicMap nics_;
+  std::vector<Port*> ports_;
+  std::vector<PortAllocatorSession*> sessions_;
+  uint32 timeout_ms_;
+  talk_base::SocketAddress stun_address_;
+  talk_base::Thread* main_;
+};
+
+}  // namespace cricket
+
+#endif  // TALK_P2P_CLIENT_CONNECTIVITYCHECKER_H_
diff --git a/talk/p2p/client/connectivitychecker_unittest.cc b/talk/p2p/client/connectivitychecker_unittest.cc
new file mode 100644
index 0000000..d63704a
--- /dev/null
+++ b/talk/p2p/client/connectivitychecker_unittest.cc
@@ -0,0 +1,367 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+
+#include <string>
+
+#include "talk/base/asynchttprequest.h"
+#include "talk/base/basicpacketsocketfactory.h"
+#include "talk/base/gunit.h"
+#include "talk/base/fakenetwork.h"
+#include "talk/base/scoped_ptr.h"
+#include "talk/base/socketaddress.h"
+#include "talk/p2p/base/relayport.h"
+#include "talk/p2p/base/stunport.h"
+#include "talk/p2p/client/connectivitychecker.h"
+#include "talk/p2p/client/httpportallocator.h"
+
+namespace cricket {
+
+static const talk_base::SocketAddress kClientAddr1("11.11.11.11", 0);
+static const talk_base::SocketAddress kClientAddr2("22.22.22.22", 0);
+static const talk_base::SocketAddress kExternalAddr("33.33.33.33", 3333);
+static const talk_base::SocketAddress kStunAddr("44.44.44.44", 4444);
+static const talk_base::SocketAddress kRelayAddr("55.55.55.55", 5555);
+static const talk_base::SocketAddress kProxyAddr("66.66.66.66", 6666);
+static const talk_base::ProxyType kProxyType = talk_base::PROXY_HTTPS;
+static const char kSessionName[] = "rtp_test";
+static const char kSessionType[] = "http://www.google.com/session/test";
+static const char kRelayHost[] = "relay.google.com";
+static const char kRelayToken[] =
+    "CAESFwoOb2phQGdvb2dsZS5jb20Q043h47MmGhBTB1rbfIXkhuarDCZe+xF6";
+static const char kBrowserAgent[] = "browser_test";
+static const char kJid[] = "a.b@c";
+static const char kUserName[] = "testuser";
+static const char kPassword[] = "testpassword";
+static const char kMagicCookie[] = "testcookie";
+static const char kRelayUdpPort[] = "4444";
+static const char kRelayTcpPort[] = "5555";
+static const char kRelaySsltcpPort[] = "6666";
+static const char kSessionId[] = "testsession";
+static const char kConnection[] = "testconnection";
+static const int kMinPort = 1000;
+static const int kMaxPort = 2000;
+
+// Fake implementation to mock away real network usage.
+class FakeRelayPort : public RelayPort {
+ public:
+  FakeRelayPort(talk_base::Thread* thread,
+                talk_base::PacketSocketFactory* factory,
+                talk_base::Network* network,
+                const talk_base::IPAddress& ip,
+                int min_port,
+                int max_port,
+                const std::string& username,
+                const std::string& password,
+                const std::string& magic_cookie)
+      : RelayPort(thread,
+                  factory,
+                  network,
+                  ip,
+                  min_port,
+                  max_port,
+                  username,
+                  password,
+                  magic_cookie) {
+  }
+
+  // Just signal that we are done.
+  virtual void PrepareAddress() {
+    SignalAddressReady(this);
+  }
+};
+
+// Fake implementation to mock away real network usage.
+class FakeStunPort : public StunPort {
+ public:
+  FakeStunPort(talk_base::Thread* thread,
+               talk_base::PacketSocketFactory* factory,
+               talk_base::Network* network,
+               const talk_base::IPAddress& ip,
+               int min_port,
+               int max_port,
+               const talk_base::SocketAddress& server_addr)
+      : StunPort(thread,
+                 factory,
+                 network,
+                 ip,
+                 min_port,
+                 max_port,
+                 server_addr) {
+  }
+
+  // Just set external address and signal that we are done.
+  virtual void PrepareAddress() {
+    AddAddress(kExternalAddr, "udp", true);
+    SignalAddressReady(this);
+  }
+};
+
+// Fake implementation to mock away real network usage by responding
+// to http requests immediately.
+class FakeHttpPortAllocatorSession : public TestHttpPortAllocatorSession {
+ public:
+  FakeHttpPortAllocatorSession(
+      HttpPortAllocator* allocator,
+      const std::string& name,
+      const std::string& session_type,
+      const std::vector<talk_base::SocketAddress>& stun_hosts,
+      const std::vector<std::string>& relay_hosts,
+      const std::string& relay_token,
+      const std::string& agent)
+      : TestHttpPortAllocatorSession(allocator,
+                                     name,
+                                     session_type,
+                                     stun_hosts,
+                                     relay_hosts,
+                                     relay_token,
+                                     agent) {
+  }
+  virtual void SendSessionRequest(const std::string& host, int port) {
+    FakeReceiveSessionResponse(host, port);
+  }
+
+  // Pass results to the real implementation.
+  void FakeReceiveSessionResponse(const std::string& host, int port) {
+    talk_base::AsyncHttpRequest* response = CreateAsyncHttpResponse(port);
+    TestHttpPortAllocatorSession::OnRequestDone(response);
+    response->Destroy(true);
+  }
+
+ private:
+  // Helper method for creating a response to a relay session request.
+  talk_base::AsyncHttpRequest* CreateAsyncHttpResponse(int port) {
+    talk_base::AsyncHttpRequest* request =
+        new talk_base::AsyncHttpRequest(kBrowserAgent);
+    std::stringstream ss;
+    ss << "username=" << kUserName << std::endl
+       << "password=" << kPassword << std::endl
+       << "magic_cookie=" << kMagicCookie << std::endl
+       << "relay.ip=" << kRelayAddr.IPAsString() << std::endl
+       << "relay.udp_port=" << kRelayUdpPort << std::endl
+       << "relay.tcp_port=" << kRelayTcpPort << std::endl
+       << "relay.ssltcp_port=" << kRelaySsltcpPort << std::endl;
+    request->response().document.reset(
+        new talk_base::MemoryStream(ss.str().c_str()));
+    request->response().set_success();
+    request->set_port(port);
+    request->set_secure(port == talk_base::HTTP_SECURE_PORT);
+    return request;
+  }
+};
+
+// Fake implementation for creating fake http sessions.
+class FakeHttpPortAllocator : public HttpPortAllocator {
+ public:
+  FakeHttpPortAllocator(talk_base::NetworkManager* network_manager,
+                        const std::string& user_agent)
+      : HttpPortAllocator(network_manager, user_agent) {
+  }
+
+  virtual PortAllocatorSession* CreateSession(const std::string& name,
+                                              const std::string& session_type) {
+    std::vector<talk_base::SocketAddress> stun_hosts;
+    stun_hosts.push_back(kStunAddr);
+    std::vector<std::string> relay_hosts;
+    relay_hosts.push_back(kRelayHost);
+    return new FakeHttpPortAllocatorSession(this,
+                                            kSessionName,
+                                            kSessionType,
+                                            stun_hosts,
+                                            relay_hosts,
+                                            kRelayToken,
+                                            kBrowserAgent);
+  }
+};
+
+class ConnectivityCheckerForTest : public ConnectivityChecker {
+ public:
+  ConnectivityCheckerForTest(talk_base::Thread* worker,
+                             const std::string& jid,
+                             const std::string& session_id,
+                             const std::string& user_agent,
+                             const std::string& relay_token,
+                             const std::string& connection)
+      : ConnectivityChecker(worker,
+                            jid,
+                            session_id,
+                            user_agent,
+                            relay_token,
+                            connection),
+        proxy_initiated_(false) {
+  }
+
+  talk_base::FakeNetworkManager* network_manager() const {
+    return network_manager_;
+  }
+
+  FakeHttpPortAllocator* port_allocator() const {
+    return fake_port_allocator_;
+  }
+
+ protected:
+  // Overridden methods for faking a real network.
+  virtual talk_base::NetworkManager* CreateNetworkManager() {
+    network_manager_ = new talk_base::FakeNetworkManager();
+    return network_manager_;
+  }
+  virtual talk_base::BasicPacketSocketFactory* CreateSocketFactory(
+      talk_base::Thread* thread) {
+    // Create socket factory, for simplicity, let it run on the current thread.
+    socket_factory_ =
+        new talk_base::BasicPacketSocketFactory(talk_base::Thread::Current());
+    return socket_factory_;
+  }
+  virtual HttpPortAllocator* CreatePortAllocator(
+      talk_base::NetworkManager* network_manager,
+      const std::string& user_agent,
+      const std::string& relay_token) {
+    fake_port_allocator_ =
+        new FakeHttpPortAllocator(network_manager, user_agent);
+    return fake_port_allocator_;
+  }
+  virtual StunPort* CreateStunPort(const PortConfiguration* config,
+                                   talk_base::Network* network) {
+    return new FakeStunPort(worker(),
+                            socket_factory_,
+                            network,
+                            network->ip(),
+                            kMinPort,
+                            kMaxPort,
+                            config->stun_address);
+  }
+  virtual RelayPort* CreateRelayPort(const PortConfiguration* config,
+                                     talk_base::Network* network) {
+    return new FakeRelayPort(worker(),
+                             socket_factory_,
+                             network,
+                             network->ip(),
+                             kMinPort,
+                             kMaxPort,
+                             config->username,
+                             config->password,
+                             config->magic_cookie);
+  }
+  virtual void InitiateProxyDetection() {
+    if (!proxy_initiated_) {
+      proxy_initiated_ = true;
+      proxy_info_.address = kProxyAddr;
+      proxy_info_.type = kProxyType;
+      SetProxyInfo(proxy_info_);
+    }
+  }
+
+  virtual talk_base::ProxyInfo GetProxyInfo() const {
+    return proxy_info_;
+  }
+
+ private:
+  talk_base::BasicPacketSocketFactory* socket_factory_;
+  FakeHttpPortAllocator* fake_port_allocator_;
+  talk_base::FakeNetworkManager* network_manager_;
+  talk_base::ProxyInfo proxy_info_;
+  bool proxy_initiated_;
+};
+
+class ConnectivityCheckerTest : public testing::Test {
+ protected:
+  void VerifyNic(const NicInfo& info,
+                 const talk_base::SocketAddress& local_address) {
+    // Verify that the external address has been set.
+    EXPECT_EQ(kExternalAddr, info.external_address);
+
+    // Verify that the stun server address has been set.
+    EXPECT_EQ(kStunAddr, info.stun_server_address);
+
+    // Verify that the media server address has been set. Don't care
+    // about port since it is different for different protocols.
+    EXPECT_EQ(kRelayAddr.ipaddr(), info.media_server_address.ipaddr());
+
+    // Verify that local ip matches.
+    EXPECT_EQ(local_address.ipaddr(), info.ip);
+
+    // Verify that we have received responses for our
+    // pings. Unsuccessful ping has rtt value -1, successful >= 0.
+    EXPECT_GE(info.stun.rtt, 0);
+    EXPECT_GE(info.udp.rtt, 0);
+    EXPECT_GE(info.tcp.rtt, 0);
+    EXPECT_GE(info.ssltcp.rtt, 0);
+
+    // If proxy has been set, verify address and type.
+    if (!info.proxy_info.address.IsNil()) {
+      EXPECT_EQ(kProxyAddr, info.proxy_info.address);
+      EXPECT_EQ(kProxyType, info.proxy_info.type);
+    }
+  }
+};
+
+// Tests a configuration with two network interfaces. Verifies that 4
+// combinations of ip/proxy are created and that all protocols are
+// tested on each combination.
+TEST_F(ConnectivityCheckerTest, TestStart) {
+  ConnectivityCheckerForTest connectivity_checker(talk_base::Thread::Current(),
+                                                  kJid,
+                                                  kSessionId,
+                                                  kBrowserAgent,
+                                                  kRelayToken,
+                                                  kConnection);
+  connectivity_checker.Initialize();
+  connectivity_checker.set_stun_address(kStunAddr);
+  connectivity_checker.network_manager()->AddInterface(kClientAddr1);
+  connectivity_checker.network_manager()->AddInterface(kClientAddr2);
+
+  connectivity_checker.Start();
+  talk_base::Thread::Current()->ProcessMessages(1000);
+
+  NicMap nics = connectivity_checker.GetResults();
+
+  // There should be 4 nics in our map. 2 for each interface added,
+  // one with proxy set and one without.
+  EXPECT_EQ(4U, nics.size());
+
+  // First verify interfaces without proxy.
+  talk_base::SocketAddress nilAddress;
+
+  // First lookup the address of the first nic combined with no proxy.
+  NicMap::iterator i = nics.find(NicId(kClientAddr1.ipaddr(), nilAddress));
+  ASSERT(i != nics.end());
+  NicInfo info = i->second;
+  VerifyNic(info, kClientAddr1);
+
+  // Then make sure the second device has been tested without proxy.
+  i = nics.find(NicId(kClientAddr2.ipaddr(), nilAddress));
+  ASSERT(i != nics.end());
+  info = i->second;
+  VerifyNic(info, kClientAddr2);
+
+  // Now verify both interfaces with proxy.
+  i = nics.find(NicId(kClientAddr1.ipaddr(), kProxyAddr));
+  ASSERT(i != nics.end());
+  info = i->second;
+  VerifyNic(info, kClientAddr1);
+
+  i = nics.find(NicId(kClientAddr2.ipaddr(), kProxyAddr));
+  ASSERT(i != nics.end());
+  info = i->second;
+  VerifyNic(info, kClientAddr2);
+};
+
+// Tests that nothing bad happens if thera are no network interfaces
+// available to check.
+TEST_F(ConnectivityCheckerTest, TestStartNoNetwork) {
+  ConnectivityCheckerForTest connectivity_checker(talk_base::Thread::Current(),
+                                                  kJid,
+                                                  kSessionId,
+                                                  kBrowserAgent,
+                                                  kRelayToken,
+                                                  kConnection);
+  connectivity_checker.Initialize();
+  connectivity_checker.Start();
+  talk_base::Thread::Current()->ProcessMessages(1000);
+
+  NicMap nics = connectivity_checker.GetResults();
+
+  // Verify that no nics where checked.
+  EXPECT_EQ(0U, nics.size());
+}
+
+}  // namespace cricket
diff --git a/talk/session/phone/fakenetworkinterface.h b/talk/session/phone/fakenetworkinterface.h
new file mode 100644
index 0000000..09c3bf7
--- /dev/null
+++ b/talk/session/phone/fakenetworkinterface.h
@@ -0,0 +1,165 @@
+/*
+ * libjingle
+ * Copyright 2004--2011, 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.
+ */
+
+#ifndef TALK_SESSION_PHONE_FAKENETWORKINTERFACE_H_
+#define TALK_SESSION_PHONE_FAKENETWORKINTERFACE_H_
+
+#include <vector>
+
+#include "talk/base/buffer.h"
+#include "talk/base/byteorder.h"
+#include "talk/base/criticalsection.h"
+#include "talk/base/messagehandler.h"
+#include "talk/base/messagequeue.h"
+#include "talk/base/thread.h"
+#include "talk/session/phone/mediachannel.h"
+
+namespace cricket {
+
+// Fake NetworkInterface that sends/receives RTP/RTCP packets.
+class FakeNetworkInterface : public MediaChannel::NetworkInterface,
+                             public talk_base::MessageHandler {
+ public:
+  FakeNetworkInterface()
+      : thread_(talk_base::Thread::Current()),
+        dest_(NULL),
+        conf_(false),
+        sendbuf_size_(-1),
+        recvbuf_size_(-1) {
+  }
+
+  void SetDestination(MediaChannel* dest) { dest_ = dest; }
+
+  // Conference mode is a mode where instead of simply forwarding the packets,
+  // the transport will send multiple copies of the packet with the specified
+  // SSRCs. This allows us to simulate receiving media from multiple sources.
+  void SetConferenceMode(bool conf, const std::vector<uint32>& ssrcs) {
+    conf_ = conf;
+    ssrcs_ = ssrcs;
+  }
+
+  int NumRtpBytes() {
+    talk_base::CritScope cs(&crit_);
+    int bytes = 0;
+    for (size_t i = 0; i < rtp_packets_.size(); ++i) {
+      bytes += rtp_packets_[i].length();
+    }
+    return bytes;
+  }
+
+  int NumRtpPackets() {
+    talk_base::CritScope cs(&crit_);
+    return rtp_packets_.size();
+  }
+
+  const talk_base::Buffer* GetRtpPacket(int index) {
+    talk_base::CritScope cs(&crit_);
+    return (index < NumRtpPackets()) ? &rtp_packets_[index] : NULL;
+  }
+
+  int NumRtcpPackets() {
+    talk_base::CritScope cs(&crit_);
+    return rtcp_packets_.size();
+  }
+
+  const talk_base::Buffer* GetRtcpPacket(int index) {
+    talk_base::CritScope cs(&crit_);
+    return (index < NumRtcpPackets()) ? &rtcp_packets_[index] : NULL;
+  }
+
+  int sendbuf_size() const { return sendbuf_size_; }
+  int recvbuf_size() const { return recvbuf_size_; }
+
+ protected:
+  virtual bool SendPacket(talk_base::Buffer* packet) {
+    talk_base::CritScope cs(&crit_);
+    rtp_packets_.push_back(*packet);
+    if (conf_) {
+      talk_base::Buffer buffer_copy(*packet);
+      for (size_t i = 0; i < ssrcs_.size(); ++i) {
+        talk_base::SetBE32(buffer_copy.data() + 8, ssrcs_[i]);
+        PostMessage(ST_RTP, buffer_copy);
+      }
+    } else {
+      PostMessage(ST_RTP, *packet);
+    }
+    return true;
+  }
+
+  virtual bool SendRtcp(talk_base::Buffer* packet) {
+    talk_base::CritScope cs(&crit_);
+    rtcp_packets_.push_back(*packet);
+    if (!conf_) {
+      // don't worry about RTCP in conf mode for now
+      PostMessage(ST_RTCP, *packet);
+    }
+    return true;
+  }
+
+  virtual int SetOption(SocketType type, talk_base::Socket::Option opt,
+                        int option) {
+    if (opt == talk_base::Socket::OPT_SNDBUF) {
+      sendbuf_size_ = option;
+    } else if (opt == talk_base::Socket::OPT_RCVBUF) {
+      recvbuf_size_ = option;
+    }
+    return 0;
+  }
+
+  void PostMessage(int id, const talk_base::Buffer& packet) {
+    thread_->Post(this, id, talk_base::WrapMessageData(packet));
+  }
+
+  virtual void OnMessage(talk_base::Message* msg) {
+    talk_base::TypedMessageData<talk_base::Buffer>* msg_data =
+        static_cast<talk_base::TypedMessageData<talk_base::Buffer>*>(
+            msg->pdata);
+    if (dest_) {
+      if (msg->message_id == ST_RTP) {
+        dest_->OnPacketReceived(&msg_data->data());
+      } else {
+        dest_->OnRtcpReceived(&msg_data->data());
+      }
+    }
+    delete msg_data;
+  }
+
+ private:
+  talk_base::Thread* thread_;
+  MediaChannel* dest_;
+  bool conf_;
+  std::vector<uint32> ssrcs_;
+  talk_base::CriticalSection crit_;
+  std::vector<talk_base::Buffer> rtp_packets_;
+  std::vector<talk_base::Buffer> rtcp_packets_;
+  int sendbuf_size_;
+  int recvbuf_size_;
+};
+
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_FAKENETWORKINTERFACE_H_
diff --git a/talk/session/phone/fakevideocapturer.h b/talk/session/phone/fakevideocapturer.h
new file mode 100644
index 0000000..335bc8d
--- /dev/null
+++ b/talk/session/phone/fakevideocapturer.h
@@ -0,0 +1,118 @@
+/*
+ * libjingle
+ * Copyright 2004--2011, 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.
+ */
+
+#ifndef TALK_SESSION_PHONE_FAKEVIDEOCAPTURER_H_
+#define TALK_SESSION_PHONE_FAKEVIDEOCAPTURER_H_
+
+#include <vector>
+
+#include "talk/session/phone/videocapturer.h"
+#include "talk/session/phone/videocommon.h"
+#include "talk/session/phone/videoframe.h"
+
+namespace cricket {
+
+// Fake video capturer that allows the test to manually pump in frames.
+class FakeVideoCapturer : public cricket::VideoCapturer {
+ public:
+  FakeVideoCapturer()
+      : running_(false),
+        next_timestamp_(0) {
+    // Default supported formats. Use ResetSupportedFormats to over write.
+    std::vector<cricket::VideoFormat> formats;
+    formats.push_back(cricket::VideoFormat(640, 480,
+        cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420));
+    formats.push_back(cricket::VideoFormat(320, 240,
+        cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420));
+    formats.push_back(cricket::VideoFormat(160, 120,
+        cricket::VideoFormat::FpsToInterval(30), cricket::FOURCC_I420));
+    ResetSupportedFormats(formats);
+  }
+  ~FakeVideoCapturer() {
+    SignalDestroyed(this);
+  }
+
+  void ResetSupportedFormats(const std::vector<cricket::VideoFormat>& formats) {
+    SetSupportedFormats(formats);
+  }
+  bool CaptureFrame() {
+    if (!GetCaptureFormat()) {
+      return false;
+    }
+    return CaptureCustomFrame(GetCaptureFormat()->width,
+                              GetCaptureFormat()->height,
+                              GetCaptureFormat()->fourcc);
+  }
+  bool CaptureCustomFrame(int width, int height, uint32 fourcc) {
+    if (!running_) {
+      return false;
+    }
+
+    // Currently, |fourcc| is always I420.
+    uint32 size = cricket::VideoFrame::SizeOf(width, height);
+    cricket::CapturedFrame frame;
+    frame.width = width;
+    frame.height = height;
+    frame.fourcc = fourcc;
+    frame.data_size = size;
+    frame.elapsed_time = frame.time_stamp = next_timestamp_;
+    next_timestamp_ += 33333333;  // 30 fps
+
+    talk_base::scoped_array<char> data(new char[size]);
+    memset(data.get(), 0, size);
+    frame.data = data.get();
+    SignalFrameCaptured(this, &frame);
+    return true;
+  }
+  sigslot::signal1<FakeVideoCapturer*> SignalDestroyed;
+
+  virtual cricket::CaptureResult Start(const cricket::VideoFormat& format) {
+    cricket::VideoFormat supported;
+    if (GetBestCaptureFormat(format, &supported)) {
+      SetCaptureFormat(&supported);
+    }
+    running_ = true;
+    return cricket::CR_SUCCESS;
+  }
+  virtual void Stop() {
+    running_ = false;
+    SetCaptureFormat(NULL);
+  }
+  virtual bool IsRunning() { return running_; }
+  bool GetPreferredFourccs(std::vector<uint32>* fourccs) {
+    fourccs->push_back(cricket::FOURCC_I420);
+    return true;
+  }
+
+ private:
+  bool running_;
+  int64 next_timestamp_;
+};
+
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_FAKEVIDEOCAPTURER_H_
diff --git a/talk/session/phone/fakevideorenderer.h b/talk/session/phone/fakevideorenderer.h
new file mode 100644
index 0000000..3227fa4
--- /dev/null
+++ b/talk/session/phone/fakevideorenderer.h
@@ -0,0 +1,77 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef TALK_SESSION_PHONE_FAKEVIDEORENDERER_H_
+#define TALK_SESSION_PHONE_FAKEVIDEORENDERER_H_
+
+#include "talk/base/sigslot.h"
+#include "talk/session/phone/videorenderer.h"
+
+namespace cricket {
+
+// Faked video renderer that has a callback for actions on rendering.
+class FakeVideoRenderer : public VideoRenderer {
+ public:
+  FakeVideoRenderer()
+      : width_(0),
+        height_(0),
+        num_set_sizes_(0),
+        num_rendered_frames_(0) {
+  }
+
+  virtual bool SetSize(int width, int height, int reserved) {
+    width_ = width;
+    height_ = height;
+    ++num_set_sizes_;
+    SignalSetSize(width, height, reserved);
+    return true;
+  }
+
+  virtual bool RenderFrame(const VideoFrame* frame) {
+    ++num_rendered_frames_;
+    SignalRenderFrame(frame);
+    return true;
+  }
+
+  int width() const { return width_; }
+  int height() const { return height_; }
+  int num_set_sizes() const { return num_set_sizes_; }
+  int num_rendered_frames() const { return num_rendered_frames_; }
+
+  sigslot::signal3<int, int, int> SignalSetSize;
+  sigslot::signal1<const VideoFrame*> SignalRenderFrame;
+
+ private:
+  int width_;
+  int height_;
+  int num_set_sizes_;
+  int num_rendered_frames_;
+};
+
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_FAKEVIDEORENDERER_H_
diff --git a/talk/session/phone/fakewebrtccommon.h b/talk/session/phone/fakewebrtccommon.h
new file mode 100644
index 0000000..1ade630
--- /dev/null
+++ b/talk/session/phone/fakewebrtccommon.h
@@ -0,0 +1,52 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#ifndef TALK_SESSION_PHONE_FAKEWEBRTCCOMMON_H_
+#define TALK_SESSION_PHONE_FAKEWEBRTCCOMMON_H_
+
+namespace cricket {
+
+#define WEBRTC_STUB(method, args) \
+  virtual int method args { return 0; }
+
+#define WEBRTC_STUB_CONST(method, args) \
+  virtual int method args const { return 0; }
+
+#define WEBRTC_FUNC(method, args) \
+  virtual int method args
+
+#define WEBRTC_FUNC_CONST(method, args) \
+  virtual int method args const
+
+#define WEBRTC_CHECK_CHANNEL(channel) \
+  if (channels_.find(channel) == channels_.end()) return -1;
+
+#define WEBRTC_ASSERT_CHANNEL(channel) \
+  ASSERT(channels_.find(channel) != channels_.end());
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_FAKEWEBRTCCOMMON_H_
diff --git a/talk/session/phone/fakewebrtcvideoengine.h b/talk/session/phone/fakewebrtcvideoengine.h
new file mode 100644
index 0000000..6269b71
--- /dev/null
+++ b/talk/session/phone/fakewebrtcvideoengine.h
@@ -0,0 +1,588 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#ifndef TALK_SESSION_PHONE_FAKEWEBRTCVIDEOENGINE_H_
+#define TALK_SESSION_PHONE_FAKEWEBRTCVIDEOENGINE_H_
+
+#include <list>
+#include <map>
+#include <vector>
+
+#include "talk/base/basictypes.h"
+#include "talk/base/stringutils.h"
+#include "talk/session/phone/codec.h"
+#include "talk/session/phone/fakewebrtccommon.h"
+#include "talk/session/phone/webrtcvie.h"
+
+namespace webrtc {
+
+bool operator==(const webrtc::VideoCodec& c1, const webrtc::VideoCodec& c2) {
+  return memcmp(&c1, &c2, sizeof(c1)) == 0;
+}
+
+}
+
+namespace cricket {
+
+#define WEBRTC_CHECK_CAPTURER(capturer) \
+  if (capturers_.find(capturer) == capturers_.end()) return -1;
+
+#define WEBRTC_ASSERT_CAPTURER(capturer) \
+  ASSERT(capturers_.find(capturer) != capturers_.end());
+
+static const int kStartVideoBitrate = 300;
+static const int kMaxVideoBitrate = 1000;
+
+// WebRtc channel id and capture id share the same number space.
+// This is how AddRenderer(renderId, ...) is able to tell if it is adding a
+// renderer for a channel or it is adding a renderer for a capturer.
+static const int kViEChannelIdBase = 0;
+static const int kViEChannelIdMax = 1000;
+static const int kViECaptureIdBase = 10000;  // Make sure there is a gap.
+static const int kViECaptureIdMax = 11000;
+
+class FakeWebRtcVideoEngine
+    : public webrtc::ViEBase,
+      public webrtc::ViECodec,
+      public webrtc::ViECapture,
+      public webrtc::ViENetwork,
+      public webrtc::ViERender,
+      public webrtc::ViERTP_RTCP,
+      public webrtc::ViEImageProcess {
+ public:
+  struct Channel {
+    Channel()
+        : capture_id_(-1),
+          has_renderer_(false),
+          render_started_(false),
+          send(false),
+          local_ssrc_(0),
+          rtcp_status_(webrtc::kRtcpNone),
+          key_frame_request_method_(webrtc::kViEKeyFrameRequestNone),
+          tmmbr_(false),
+          nack_(false) {
+      memset(&send_codec, 0, sizeof(send_codec));
+    }
+    int capture_id_;
+    bool has_renderer_;
+    bool render_started_;
+    bool send;
+    int local_ssrc_;
+    std::string cname_;
+    webrtc::ViERTCPMode rtcp_status_;
+    webrtc::ViEKeyFrameRequestMethod key_frame_request_method_;
+    bool tmmbr_;
+    bool nack_;
+    std::vector<webrtc::VideoCodec> recv_codecs;
+    webrtc::VideoCodec send_codec;
+  };
+  class Capturer : public webrtc::ViEExternalCapture {
+   public:
+    Capturer() : channel_id_(-1) { }
+    int channel_id() const { return channel_id_; }
+    void set_channel_id(int channel_id) { channel_id_ = channel_id; }
+
+    // From ViEExternalCapture
+    virtual int IncomingFrame(unsigned char* videoFrame,
+                              unsigned int videoFrameLength,
+                              unsigned short width,
+                              unsigned short height,
+                              webrtc::RawVideoType videoType,
+                              unsigned long long captureTime) {
+      return 0;
+    }
+    virtual int IncomingFrameI420(
+        const webrtc::ViEVideoFrameI420& video_frame,
+        unsigned long long captureTime) {
+      return 0;
+    }
+
+   private:
+    int channel_id_;
+  };
+
+  FakeWebRtcVideoEngine(const cricket::VideoCodec* const* codecs,
+                        int num_codecs)
+      : inited_(false),
+        last_channel_(kViEChannelIdBase - 1),
+        fail_create_channel_(false),
+        last_capturer_(kViECaptureIdBase - 1),
+        fail_alloc_capturer_(false),
+        codecs_(codecs),
+        num_codecs_(num_codecs) {
+  }
+
+  ~FakeWebRtcVideoEngine() {
+    ASSERT(0 == channels_.size());
+    ASSERT(0 == capturers_.size());
+  }
+  bool IsInited() const { return inited_; }
+
+  int GetLastChannel() const { return last_channel_; }
+  int GetNumChannels() const { return channels_.size(); }
+  void set_fail_create_channel(bool fail_create_channel) {
+    fail_create_channel_ = fail_create_channel;
+  }
+
+  int GetLastCapturer() const { return last_capturer_; }
+  int GetNumCapturers() const { return capturers_.size(); }
+  void set_fail_alloc_capturer(bool fail_alloc_capturer) {
+    fail_alloc_capturer_ = fail_alloc_capturer;
+  }
+
+  int GetCaptureId(int channel) const {
+    WEBRTC_ASSERT_CHANNEL(channel);
+    return channels_.find(channel)->second->capture_id_;
+  }
+  bool GetHasRenderer(int channel) const {
+    WEBRTC_ASSERT_CHANNEL(channel);
+    return channels_.find(channel)->second->has_renderer_;
+  }
+  bool GetRenderStarted(int channel) const {
+    WEBRTC_ASSERT_CHANNEL(channel);
+    return channels_.find(channel)->second->render_started_;
+  }
+  bool GetSend(int channel) const {
+    WEBRTC_ASSERT_CHANNEL(channel);
+    return channels_.find(channel)->second->send;
+  }
+  int GetCaptureChannelId(int capture_id) const {
+    WEBRTC_ASSERT_CAPTURER(capture_id);
+    return capturers_.find(capture_id)->second->channel_id();
+  }
+  webrtc::ViERTCPMode GetRtcpStatus(int channel) const {
+    WEBRTC_ASSERT_CHANNEL(channel);
+    return channels_.find(channel)->second->rtcp_status_;
+  }
+  webrtc::ViEKeyFrameRequestMethod GetKeyFrameRequestMethod(int channel) const {
+    WEBRTC_ASSERT_CHANNEL(channel);
+    return channels_.find(channel)->second->key_frame_request_method_;
+  }
+  bool GetTmmbrStatus(int channel) const {
+    WEBRTC_ASSERT_CHANNEL(channel);
+    return channels_.find(channel)->second->tmmbr_;
+  }
+  bool GetNackStatus(int channel) const {
+    WEBRTC_ASSERT_CHANNEL(channel);
+    return channels_.find(channel)->second->nack_;
+  }
+
+  bool ReceiveCodecRegistered(int channel,
+                              const webrtc::VideoCodec& codec) const {
+    WEBRTC_ASSERT_CHANNEL(channel);
+    const std::vector<webrtc::VideoCodec>& codecs =
+      channels_.find(channel)->second->recv_codecs;
+    return std::find(codecs.begin(), codecs.end(), codec) != codecs.end();
+  };
+
+  WEBRTC_STUB(Release, ());
+
+  // webrtc::ViEBase
+  WEBRTC_FUNC(Init, ()) {
+    inited_ = true;
+    return 0;
+  };
+  WEBRTC_STUB(SetVoiceEngine, (webrtc::VoiceEngine*));
+  WEBRTC_FUNC(CreateChannel, (int& channel)) {  // NOLINT
+    if (fail_create_channel_) {
+      return -1;
+    }
+    if (kViEChannelIdMax == last_channel_) {
+      return -1;
+    }
+    Channel* ch = new Channel();
+    channels_[++last_channel_] = ch;
+    channel = last_channel_;
+    return 0;
+  };
+  WEBRTC_STUB(CreateChannel, (int&, int));
+  WEBRTC_FUNC(DeleteChannel, (const int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    delete channels_[channel];
+    channels_.erase(channel);
+    return 0;
+  }
+  WEBRTC_STUB(ConnectAudioChannel, (const int, const int));
+  WEBRTC_STUB(DisconnectAudioChannel, (const int));
+  WEBRTC_FUNC(StartSend, (const int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->send = true;
+    return 0;
+  }
+  WEBRTC_FUNC(StopSend, (const int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->send = false;
+    return 0;
+  }
+  WEBRTC_STUB(StartReceive, (const int));
+  WEBRTC_STUB(StopReceive, (const int));
+  WEBRTC_STUB(RegisterObserver, (webrtc::ViEBaseObserver&));
+  WEBRTC_STUB(DeregisterObserver, ());
+  WEBRTC_STUB(GetVersion, (char version[1024]));
+  WEBRTC_STUB(LastError, ());
+
+  // webrtc::ViECodec
+  WEBRTC_FUNC(NumberOfCodecs, ()) const {
+    return num_codecs_;
+  };
+  WEBRTC_FUNC(GetCodec, (const unsigned char list_number,
+                         webrtc::VideoCodec& out_codec)) const {
+    if (list_number >= NumberOfCodecs()) {
+      return -1;
+    }
+    memset(&out_codec, 0, sizeof(out_codec));
+    const cricket::VideoCodec& c(*codecs_[list_number]);
+    if ("I420" == c.name) {
+      out_codec.codecType = webrtc::kVideoCodecI420;
+    }
+    else if ("VP8" == c.name) {
+      out_codec.codecType = webrtc::kVideoCodecVP8;
+    } else {
+      out_codec.codecType = webrtc::kVideoCodecUnknown;
+    }
+    talk_base::strcpyn(out_codec.plName, sizeof(out_codec.plName),
+                       c.name.c_str());
+    out_codec.plType = c.id;
+    out_codec.width = c.width;
+    out_codec.height = c.height;
+    out_codec.startBitrate = kStartVideoBitrate;
+    out_codec.maxBitrate = kMaxVideoBitrate;
+    out_codec.minBitrate = kStartVideoBitrate;
+    out_codec.maxFramerate = c.framerate;
+    return 0;
+  };
+  WEBRTC_FUNC(SetSendCodec, (const int channel,
+                             const webrtc::VideoCodec& codec)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->send_codec = codec;
+    return 0;
+  };
+  WEBRTC_FUNC(GetSendCodec, (const int channel,
+                             webrtc::VideoCodec& codec)) const {  // NOLINT
+    WEBRTC_CHECK_CHANNEL(channel);
+    codec = channels_.find(channel)->second->send_codec;
+    return 0;
+  };
+  WEBRTC_FUNC(SetReceiveCodec, (const int channel,
+                                const webrtc::VideoCodec& codec)) {  // NOLINT
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->recv_codecs.push_back(codec);
+    return 0;
+  };
+  WEBRTC_STUB_CONST(GetReceiveCodec, (const int, webrtc::VideoCodec&));
+  WEBRTC_STUB_CONST(GetCodecConfigParameters, (const int,
+      unsigned char*, unsigned char&));
+  WEBRTC_STUB(SetImageScaleStatus, (const int, const bool));
+  WEBRTC_STUB_CONST(GetSendCodecStastistics, (const int,
+      unsigned int&, unsigned int&));
+  WEBRTC_STUB_CONST(GetReceiveCodecStastistics, (const int,
+      unsigned int&, unsigned int&));
+  virtual unsigned int GetDiscardedPackets(const int channel) const {
+    return 0;
+  }
+
+  WEBRTC_STUB(SetKeyFrameRequestCallbackStatus, (const int, const bool));
+  WEBRTC_STUB(SetSignalKeyPacketLossStatus, (const int, const bool,
+      const bool));
+  WEBRTC_STUB(RegisterEncoderObserver, (const int,
+      webrtc::ViEEncoderObserver&));
+  WEBRTC_STUB(DeregisterEncoderObserver, (const int));
+  WEBRTC_STUB(RegisterDecoderObserver, (const int,
+      webrtc::ViEDecoderObserver&));
+  WEBRTC_STUB(DeregisterDecoderObserver, (const int));
+  WEBRTC_STUB(SendKeyFrame, (const int));
+  WEBRTC_STUB(WaitForFirstKeyFrame, (const int, const bool));
+  WEBRTC_STUB(SetInverseH263Logic, (int, bool));
+
+  // webrtc::ViECapture
+  WEBRTC_STUB(NumberOfCaptureDevices, ());
+  WEBRTC_STUB(GetCaptureDevice, (unsigned int, char*,
+      const unsigned int, char*, const unsigned int));
+  WEBRTC_STUB(AllocateCaptureDevice, (const char*, const unsigned int, int&));
+  WEBRTC_FUNC(AllocateExternalCaptureDevice,
+              (int& capture_id, webrtc::ViEExternalCapture *&capture)) {
+    if (fail_alloc_capturer_) {
+      return -1;
+    }
+    if (kViECaptureIdMax == last_capturer_) {
+      return -1;
+    }
+    Capturer* cap = new Capturer();
+    capturers_[++last_capturer_] = cap;
+    capture_id = last_capturer_;
+    capture = static_cast<webrtc::ViEExternalCapture *>(cap);
+    return 0;
+  }
+  WEBRTC_STUB(AllocateCaptureDevice, (webrtc::VideoCaptureModule&, int&));
+  WEBRTC_FUNC(ReleaseCaptureDevice, (const int capture_id)) {
+    WEBRTC_CHECK_CAPTURER(capture_id);
+    delete capturers_[capture_id];
+    capturers_.erase(capture_id);
+    return 0;
+  }
+  WEBRTC_FUNC(ConnectCaptureDevice, (const int capture_id,
+                                     const int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    WEBRTC_CHECK_CAPTURER(capture_id);
+    channels_[channel]->capture_id_ = capture_id;
+    capturers_[capture_id]->set_channel_id(channel);
+    return 0;
+  }
+  WEBRTC_FUNC(DisconnectCaptureDevice, (const int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    int capture_id = channels_[channel]->capture_id_;
+    WEBRTC_CHECK_CAPTURER(capture_id);
+    channels_[channel]->capture_id_ = -1;
+    capturers_[capture_id]->set_channel_id(-1);
+    return 0;
+  }
+  WEBRTC_STUB(StartCapture, (const int, const webrtc::CaptureCapability));
+  WEBRTC_STUB(StopCapture, (const int));
+  WEBRTC_STUB(SetRotateCapturedFrames, (const int,
+      const webrtc::RotateCapturedFrame));
+  WEBRTC_STUB(SetCaptureDelay, (const int, const unsigned int));
+  WEBRTC_STUB(NumberOfCapabilities, (const char*, const unsigned int));
+  WEBRTC_STUB(GetCaptureCapability, (const char*, const unsigned int,
+      const unsigned int, webrtc::CaptureCapability&));
+  WEBRTC_STUB(ShowCaptureSettingsDialogBox, (const char*, const unsigned int,
+      const char*, void*, const unsigned int, const unsigned int));
+  WEBRTC_STUB(GetOrientation, (const char*, webrtc::RotateCapturedFrame&));
+  WEBRTC_STUB(EnableBrightnessAlarm, (const int, const bool));
+  WEBRTC_STUB(RegisterObserver, (const int, webrtc::ViECaptureObserver&));
+  WEBRTC_STUB(DeregisterObserver, (const int));
+
+  // webrtc::ViENetwork
+  WEBRTC_STUB(SetLocalReceiver, (const int, const unsigned short,
+      const unsigned short, const char*));
+  WEBRTC_STUB(GetLocalReceiver, (const int, unsigned short&,
+      unsigned short&, char*));
+  WEBRTC_STUB(SetSendDestination, (const int, const char*, const unsigned short,
+      const unsigned short, const unsigned short, const unsigned short));
+  WEBRTC_STUB(GetSendDestination, (const int, char*, unsigned short&,
+      unsigned short&, unsigned short&, unsigned short&));
+  WEBRTC_STUB(RegisterSendTransport, (const int, webrtc::Transport&));
+  WEBRTC_STUB(DeregisterSendTransport, (const int));
+  WEBRTC_STUB(ReceivedRTPPacket, (const int, const void*, const int));
+  WEBRTC_STUB(ReceivedRTCPPacket, (const int, const void*, const int));
+  WEBRTC_STUB(GetSourceInfo, (const int, unsigned short&, unsigned short&,
+                              char*, unsigned int));
+  WEBRTC_STUB(GetLocalIP, (char*, bool));
+  WEBRTC_STUB(EnableIPv6, (int));
+  // Not using WEBRTC_STUB due to bool return value
+  virtual bool IsIPv6Enabled(int channel) { return true; }
+  WEBRTC_STUB(SetSourceFilter, (const int, const unsigned short,
+      const unsigned short, const char*));
+  WEBRTC_STUB(GetSourceFilter, (const int, unsigned short&,
+      unsigned short&, char*));
+  WEBRTC_STUB(SetSendToS, (const int, const int, const bool));
+  WEBRTC_STUB(GetSendToS, (const int, int&, bool&));
+  WEBRTC_STUB(SetSendGQoS, (const int, const bool, const int, const int));
+  WEBRTC_STUB(GetSendGQoS, (const int, bool&, int&, int&));
+  WEBRTC_STUB(SetMTU, (int, unsigned int));
+  WEBRTC_STUB(SetPacketTimeoutNotification, (const int, bool, int));
+  WEBRTC_STUB(RegisterObserver, (const int, webrtc::ViENetworkObserver&));
+  WEBRTC_STUB(SetPeriodicDeadOrAliveStatus, (const int, const bool,
+    const unsigned int));
+  WEBRTC_STUB(SendUDPPacket, (const int, const void*, const unsigned int,
+      int&, bool));
+
+  // webrtc::ViERender
+  WEBRTC_STUB(RegisterVideoRenderModule, (webrtc::VideoRender&));
+  WEBRTC_STUB(DeRegisterVideoRenderModule, (webrtc::VideoRender&));
+  WEBRTC_STUB(AddRenderer, (const int, void*, const unsigned int, const float,
+      const float, const float, const float));
+  WEBRTC_FUNC(RemoveRenderer, (const int render_id)) {
+    if (IsCapturerId(render_id)) {
+      WEBRTC_CHECK_CAPTURER(render_id);
+      return 0;
+    } else if (IsChannelId(render_id)) {
+      WEBRTC_CHECK_CHANNEL(render_id);
+      channels_[render_id]->has_renderer_ = false;
+      return 0;
+    }
+    return -1;
+  }
+  WEBRTC_FUNC(StartRender, (const int render_id)) {
+    if (IsCapturerId(render_id)) {
+      WEBRTC_CHECK_CAPTURER(render_id);
+      return 0;
+    } else if (IsChannelId(render_id)) {
+      WEBRTC_CHECK_CHANNEL(render_id);
+      channels_[render_id]->render_started_ = true;
+      return 0;
+    }
+    return -1;
+  }
+  WEBRTC_FUNC(StopRender, (const int render_id)) {
+    if (IsCapturerId(render_id)) {
+      WEBRTC_CHECK_CAPTURER(render_id);
+      return 0;
+    } else if (IsChannelId(render_id)) {
+      WEBRTC_CHECK_CHANNEL(render_id);
+      channels_[render_id]->render_started_ = false;
+      return 0;
+    }
+    return -1;
+  }
+  WEBRTC_STUB(ConfigureRender, (int, const unsigned int, const float,
+      const float, const float, const float));
+  WEBRTC_STUB(MirrorRenderStream, (const int, const bool, const bool,
+      const bool));
+  WEBRTC_FUNC(AddRenderer, (const int render_id,
+                            webrtc::RawVideoType video_type,
+                            webrtc::ExternalRenderer* renderer)) {
+    if (IsCapturerId(render_id)) {
+      WEBRTC_CHECK_CAPTURER(render_id);
+      return 0;
+    } else if (IsChannelId(render_id)) {
+      WEBRTC_CHECK_CHANNEL(render_id);
+      channels_[render_id]->has_renderer_ = true;
+      return 0;
+    }
+    return -1;
+  }
+
+  // webrtc::ViERTP_RTCP
+  WEBRTC_FUNC(SetLocalSSRC, (const int channel,
+                             const unsigned int ssrc,
+                             const webrtc::StreamType usage,
+                             const unsigned char simulcast_idx)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->local_ssrc_ = ssrc;
+    return 0;
+  }
+  WEBRTC_STUB_CONST(SetRemoteSSRCType, (const int,
+        const webrtc::StreamType, const unsigned int));
+
+  WEBRTC_FUNC_CONST(GetLocalSSRC, (const int channel,
+                                   unsigned int& ssrc)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    ssrc = channels_.find(channel)->second->local_ssrc_;
+    return 0;
+  }
+  WEBRTC_STUB_CONST(GetRemoteSSRC, (const int, unsigned int&));
+      WEBRTC_STUB_CONST(GetRemoteCSRCs, (const int, unsigned int*));
+  WEBRTC_STUB(SetStartSequenceNumber, (const int, unsigned short));
+  WEBRTC_FUNC(SetRTCPStatus,
+              (const int channel, const webrtc::ViERTCPMode mode)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->rtcp_status_ = mode;
+    return 0;
+  }
+  WEBRTC_STUB(GetRTCPStatus, (const int, webrtc::ViERTCPMode&));
+  WEBRTC_FUNC(SetRTCPCName, (const int channel,
+                             const char rtcp_cname[KMaxRTCPCNameLength])) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->cname_.assign(rtcp_cname);
+    return 0;
+  }
+  WEBRTC_FUNC(GetRTCPCName, (const int channel,
+                             char rtcp_cname[KMaxRTCPCNameLength])) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    talk_base::strcpyn(rtcp_cname, KMaxRTCPCNameLength,
+                       channels_[channel]->cname_.c_str());
+    return 0;
+  }
+  WEBRTC_STUB_CONST(GetRemoteRTCPCName, (const int, char*));
+  WEBRTC_STUB(SendApplicationDefinedRTCPPacket, (const int, const unsigned char,
+      unsigned int, const char*, unsigned short));
+  WEBRTC_FUNC(SetNACKStatus, (const int channel, const bool enable)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->nack_ = enable;
+    return 0;
+  }
+  WEBRTC_STUB(SetFECStatus, (const int, const bool, const unsigned char,
+      const unsigned char));
+  WEBRTC_STUB(SetHybridNACKFECStatus, (const int, const bool,
+      const unsigned char, const unsigned char));
+  WEBRTC_FUNC(SetKeyFrameRequestMethod,
+              (const int channel,
+               const webrtc::ViEKeyFrameRequestMethod method)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->key_frame_request_method_ = method;
+    return 0;
+  }
+  WEBRTC_FUNC(SetTMMBRStatus, (const int channel, const bool enable)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->tmmbr_ = enable;
+    return 0;
+  }
+  WEBRTC_STUB_CONST(GetReceivedRTCPStatistics, (const int, unsigned short&,
+      unsigned int&, unsigned int&, unsigned int&, int&));
+  WEBRTC_STUB_CONST(GetSentRTCPStatistics, (const int, unsigned short&,
+      unsigned int&, unsigned int&, unsigned int&, int&));
+  WEBRTC_STUB_CONST(GetRTPStatistics, (const int, unsigned int&, unsigned int&,
+      unsigned int&, unsigned int&));
+  WEBRTC_STUB_CONST(GetBandwidthUsage, (const int, unsigned int&,
+      unsigned int&, unsigned int&, unsigned int&));
+
+  WEBRTC_STUB(SetRTPKeepAliveStatus, (const int, bool, const char,
+      const unsigned int));
+  WEBRTC_STUB(GetRTPKeepAliveStatus, (const int, bool&, char&, unsigned int&));
+  WEBRTC_STUB(StartRTPDump, (const int, const char*, webrtc::RTPDirections));
+  WEBRTC_STUB(StopRTPDump, (const int, webrtc::RTPDirections));
+  WEBRTC_STUB(RegisterRTPObserver, (const int, webrtc::ViERTPObserver&));
+  WEBRTC_STUB(DeregisterRTPObserver, (const int));
+  WEBRTC_STUB(RegisterRTCPObserver, (const int, webrtc::ViERTCPObserver&));
+  WEBRTC_STUB(DeregisterRTCPObserver, (const int));
+
+  // webrtc::ViEImageProcess
+  WEBRTC_STUB(RegisterCaptureEffectFilter, (const int,
+      webrtc::ViEEffectFilter&));
+  WEBRTC_STUB(DeregisterCaptureEffectFilter, (const int));
+  WEBRTC_STUB(RegisterSendEffectFilter, (const int,
+      webrtc::ViEEffectFilter&));
+  WEBRTC_STUB(DeregisterSendEffectFilter, (const int));
+  WEBRTC_STUB(RegisterRenderEffectFilter, (const int,
+      webrtc::ViEEffectFilter&));
+  WEBRTC_STUB(DeregisterRenderEffectFilter, (const int));
+  WEBRTC_STUB(EnableDeflickering, (const int, const bool));
+  WEBRTC_STUB(EnableDenoising, (const int, const bool));
+  WEBRTC_STUB(EnableColorEnhancement, (const int, const bool));
+
+ private:
+  bool IsChannelId(int id) const {
+    return (id >= kViEChannelIdBase && id <= kViEChannelIdMax);
+  }
+  bool IsCapturerId(int id) const {
+    return (id >= kViECaptureIdBase && id <= kViECaptureIdMax);
+  }
+
+  bool inited_;
+  int last_channel_;
+  std::map<int, Channel*> channels_;
+  bool fail_create_channel_;
+  int last_capturer_;
+  std::map<int, Capturer*> capturers_;
+  bool fail_alloc_capturer_;
+  const cricket::VideoCodec* const* codecs_;
+  int num_codecs_;
+};
+
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_FAKEWEBRTCVIDEOENGINE_H_
diff --git a/talk/session/phone/fakewebrtcvoiceengine.h b/talk/session/phone/fakewebrtcvoiceengine.h
new file mode 100644
index 0000000..5b3ff0c
--- /dev/null
+++ b/talk/session/phone/fakewebrtcvoiceengine.h
@@ -0,0 +1,852 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#ifndef TALK_SESSION_PHONE_FAKEWEBRTCVOICEENGINE_H_
+#define TALK_SESSION_PHONE_FAKEWEBRTCVOICEENGINE_H_
+
+#include <list>
+#include <map>
+#include <vector>
+
+#include "talk/base/basictypes.h"
+#include "talk/base/stringutils.h"
+#include "talk/session/phone/codec.h"
+#include "talk/session/phone/fakewebrtccommon.h"
+#include "talk/session/phone/voiceprocessor.h"
+#include "talk/session/phone/webrtcvoe.h"
+
+namespace cricket {
+
+static const char kFakeDefaultDeviceName[] = "Fake Default";
+static const int kFakeDefaultDeviceId = -1;
+static const char kFakeDeviceName[] = "Fake Device";
+#ifdef WIN32
+static const int kFakeDeviceId = 0;
+#else
+static const int kFakeDeviceId = 1;
+#endif
+
+class FakeWebRtcVoiceEngine
+    : public webrtc::VoEAudioProcessing,
+      public webrtc::VoEBase, public webrtc::VoECodec, public webrtc::VoEDtmf,
+      public webrtc::VoEFile, public webrtc::VoEHardware,
+      public webrtc::VoEExternalMedia, public webrtc::VoENetEqStats,
+      public webrtc::VoENetwork, public webrtc::VoERTP_RTCP,
+      public webrtc::VoEVideoSync, public webrtc::VoEVolumeControl {
+ public:
+  struct Channel {
+    Channel()
+        : external_transport(false),
+          send(false),
+          playout(false),
+          file(false),
+          vad(false),
+          fec(false),
+          cn8_type(13),
+          cn16_type(105),
+          dtmf_type(106),
+          fec_type(117),
+          send_ssrc(0),
+          level_header_ext_(-1) {
+      memset(&send_codec, 0, sizeof(send_codec));
+    }
+    bool external_transport;
+    bool send;
+    bool playout;
+    bool file;
+    bool vad;
+    bool fec;
+    int cn8_type;
+    int cn16_type;
+    int dtmf_type;
+    int fec_type;
+    uint32 send_ssrc;
+    int level_header_ext_;
+    std::vector<webrtc::CodecInst> recv_codecs;
+    webrtc::CodecInst send_codec;
+    std::list<std::string> packets;
+  };
+
+  FakeWebRtcVoiceEngine(const cricket::AudioCodec* const* codecs,
+                        int num_codecs)
+      : inited_(false),
+        last_channel_(-1),
+        fail_create_channel_(false),
+        codecs_(codecs),
+        num_codecs_(num_codecs),
+        ec_enabled_(false),
+        ns_enabled_(false),
+        ec_mode_(webrtc::kEcDefault),
+        ns_mode_(webrtc::kNsDefault),
+        observer_(NULL),
+        playout_fail_channel_(-1),
+        send_fail_channel_(-1),
+        fail_start_recording_microphone_(false),
+        recording_microphone_(false),
+        media_processor_(NULL) {
+    memset(&agc_config_, 0, sizeof(agc_config_));
+  }
+  ~FakeWebRtcVoiceEngine() {
+    // Ought to have all been deleted by the WebRtcVoiceMediaChannel
+    // destructors, but just in case ...
+    for (std::map<int, Channel*>::const_iterator i = channels_.begin();
+         i != channels_.end(); ++i) {
+      delete i->second;
+    }
+  }
+  bool IsExternalMediaProcessorRegistered() const {
+    return media_processor_ != NULL;
+  }
+  bool IsInited() const { return inited_; }
+  int GetLastChannel() const { return last_channel_; }
+  int GetNumChannels() const { return channels_.size(); }
+  bool GetPlayout(int channel) {
+    return channels_[channel]->playout;
+  }
+  bool GetSend(int channel) {
+    return channels_[channel]->send;
+  }
+  bool GetRecordingMicrophone() {
+    return recording_microphone_;
+  }
+  bool GetVAD(int channel) {
+    return channels_[channel]->vad;
+  }
+  bool GetFEC(int channel) {
+    return channels_[channel]->fec;
+  }
+  int GetSendCNPayloadType(int channel, bool wideband) {
+    return (wideband) ?
+        channels_[channel]->cn16_type :
+        channels_[channel]->cn8_type;
+  }
+  int GetSendTelephoneEventPayloadType(int channel) {
+    return channels_[channel]->dtmf_type;
+  }
+  int GetSendFECPayloadType(int channel) {
+    return channels_[channel]->fec_type;
+  }
+  bool CheckPacket(int channel, const void* data, size_t len) {
+    bool result = !CheckNoPacket(channel);
+    if (result) {
+      std::string packet = channels_[channel]->packets.front();
+      result = (packet == std::string(static_cast<const char*>(data), len));
+      channels_[channel]->packets.pop_front();
+    }
+    return result;
+  }
+  bool CheckNoPacket(int channel) {
+    return channels_[channel]->packets.empty();
+  }
+  void TriggerCallbackOnError(int channel_num, int err_code) {
+    ASSERT(observer_ != NULL);
+    observer_->CallbackOnError(channel_num, err_code);
+  }
+  void set_playout_fail_channel(int channel) {
+    playout_fail_channel_ = channel;
+  }
+  void set_send_fail_channel(int channel) {
+    send_fail_channel_ = channel;
+  }
+  void set_fail_start_recording_microphone(
+      bool fail_start_recording_microphone) {
+    fail_start_recording_microphone_ = fail_start_recording_microphone;
+  }
+  void set_fail_create_channel(bool fail_create_channel) {
+    fail_create_channel_ = fail_create_channel;
+  }
+  void TriggerProcessPacket(MediaProcessorDirection direction) {
+    webrtc::ProcessingTypes pt =
+        (direction == cricket::MPD_TX) ?
+            webrtc::kRecordingPerChannel : webrtc::kPlaybackAllChannelsMixed;
+    if (media_processor_ != NULL) {
+      media_processor_->Process(0,
+                                pt,
+                                NULL,
+                                0,
+                                0,
+                                true);
+    }
+  }
+
+  WEBRTC_STUB(Release, ());
+
+  // webrtc::VoEBase
+  WEBRTC_FUNC(RegisterVoiceEngineObserver, (
+      webrtc::VoiceEngineObserver& observer)) {
+    observer_ = &observer;
+    return 0;
+  }
+  WEBRTC_STUB(DeRegisterVoiceEngineObserver, ());
+  WEBRTC_STUB(RegisterAudioDeviceModule, (webrtc::AudioDeviceModule& adm));
+  WEBRTC_STUB(DeRegisterAudioDeviceModule, ());
+
+  WEBRTC_FUNC(Init, (webrtc::AudioDeviceModule* adm)) {
+    inited_ = true;
+    return 0;
+  }
+  WEBRTC_FUNC(Terminate, ()) {
+    inited_ = false;
+    return 0;
+  }
+  WEBRTC_STUB(MaxNumOfChannels, ());
+  WEBRTC_FUNC(CreateChannel, ()) {
+    if (fail_create_channel_) {
+      return -1;
+    }
+    Channel* ch = new Channel();
+    for (int i = 0; i < NumOfCodecs(); ++i) {
+      webrtc::CodecInst codec;
+      GetCodec(i, codec);
+      ch->recv_codecs.push_back(codec);
+    }
+    channels_[++last_channel_] = ch;
+    return last_channel_;
+  }
+  WEBRTC_FUNC(DeleteChannel, (int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    delete channels_[channel];
+    channels_.erase(channel);
+    return 0;
+  }
+  WEBRTC_STUB(SetLocalReceiver, (int channel, int port, int RTCPport,
+                                 const char ipaddr[64],
+                                 const char multiCastAddr[64]));
+  WEBRTC_STUB(GetLocalReceiver, (int channel, int& port, int& RTCPport,
+                                 char ipaddr[64]));
+  WEBRTC_STUB(SetSendDestination, (int channel, int port,
+                                   const char ipaddr[64],
+                                   int sourcePort, int RTCPport));
+  WEBRTC_STUB(GetSendDestination, (int channel, int& port, char ipaddr[64],
+                                   int& sourcePort, int& RTCPport));
+  WEBRTC_STUB(StartReceive, (int channel));
+  WEBRTC_FUNC(StartPlayout, (int channel)) {
+    if (playout_fail_channel_ != channel) {
+      WEBRTC_CHECK_CHANNEL(channel);
+      channels_[channel]->playout = true;
+      return 0;
+    } else {
+      // When playout_fail_channel_ == channel, fail the StartPlayout on this
+      // channel.
+      return -1;
+    }
+  }
+  WEBRTC_FUNC(StartSend, (int channel)) {
+    if (send_fail_channel_ != channel) {
+      WEBRTC_CHECK_CHANNEL(channel);
+      channels_[channel]->send = true;
+      return 0;
+    } else {
+      // When send_fail_channel_ == channel, fail the StartSend on this
+      // channel.
+      return -1;
+    }
+  }
+  WEBRTC_STUB(StopReceive, (int channel));
+  WEBRTC_FUNC(StopPlayout, (int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->playout = false;
+    return 0;
+  }
+  WEBRTC_FUNC(StopSend, (int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->send = false;
+    return 0;
+  }
+  WEBRTC_STUB(GetVersion, (char version[1024]));
+  WEBRTC_STUB(LastError, ());
+  WEBRTC_STUB(SetOnHoldStatus, (int, bool, webrtc::OnHoldModes));
+  WEBRTC_STUB(GetOnHoldStatus, (int, bool&, webrtc::OnHoldModes&));
+  WEBRTC_STUB(SetNetEQPlayoutMode, (int, webrtc::NetEqModes));
+  WEBRTC_STUB(GetNetEQPlayoutMode, (int, webrtc::NetEqModes&));
+  WEBRTC_STUB(SetNetEQBGNMode, (int, webrtc::NetEqBgnModes));
+  WEBRTC_STUB(GetNetEQBGNMode, (int, webrtc::NetEqBgnModes&));
+
+  // webrtc::VoECodec
+  WEBRTC_FUNC(NumOfCodecs, ()) {
+    return num_codecs_;
+  }
+  WEBRTC_FUNC(GetCodec, (int index, webrtc::CodecInst& codec)) {
+    if (index < 0 || index >= NumOfCodecs()) {
+      return -1;
+    }
+    const cricket::AudioCodec& c(*codecs_[index]);
+    codec.pltype = c.id;
+    talk_base::strcpyn(codec.plname, sizeof(codec.plname), c.name.c_str());
+    codec.plfreq = c.clockrate;
+    codec.pacsize = 0;
+    codec.channels = c.channels;
+    codec.rate = c.bitrate;
+    return 0;
+  }
+  WEBRTC_FUNC(SetSendCodec, (int channel, const webrtc::CodecInst& codec)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->send_codec = codec;
+    return 0;
+  }
+  WEBRTC_FUNC(GetSendCodec, (int channel, webrtc::CodecInst& codec)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    codec = channels_[channel]->send_codec;
+    return 0;
+  }
+  WEBRTC_STUB(GetRecCodec, (int channel, webrtc::CodecInst& codec));
+  WEBRTC_STUB(SetAMREncFormat, (int channel, webrtc::AmrMode mode));
+  WEBRTC_STUB(SetAMRDecFormat, (int channel, webrtc::AmrMode mode));
+  WEBRTC_STUB(SetAMRWbEncFormat, (int channel, webrtc::AmrMode mode));
+  WEBRTC_STUB(SetAMRWbDecFormat, (int channel, webrtc::AmrMode mode));
+  WEBRTC_STUB(SetISACInitTargetRate, (int channel, int rateBps,
+                                      bool useFixedFrameSize));
+  WEBRTC_STUB(SetISACMaxRate, (int channel, int rateBps));
+  WEBRTC_STUB(SetISACMaxPayloadSize, (int channel, int sizeBytes));
+  WEBRTC_FUNC(SetRecPayloadType, (int channel,
+                                  const webrtc::CodecInst& codec)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    Channel* ch = channels_[channel];
+    // Check if something else already has this slot.
+    if (codec.pltype != -1) {
+      for (std::vector<webrtc::CodecInst>::iterator it =
+          ch->recv_codecs.begin(); it != ch->recv_codecs.end(); ++it) {
+        if (it->pltype == codec.pltype) {
+          return -1;
+        }
+      }
+    }
+    // Otherwise try to find this codec and update its payload type.
+    for (std::vector<webrtc::CodecInst>::iterator it = ch->recv_codecs.begin();
+         it != ch->recv_codecs.end(); ++it) {
+      if (strcmp(it->plname, codec.plname) == 0 &&
+          it->plfreq == codec.plfreq) {
+        it->pltype = codec.pltype;
+        return 0;
+      }
+    }
+    return -1;  // not found
+  }
+  WEBRTC_FUNC(SetSendCNPayloadType, (int channel, int type,
+                                     webrtc::PayloadFrequencies frequency)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    if (frequency == webrtc::kFreq8000Hz) {
+      channels_[channel]->cn8_type = type;
+    } else if (frequency == webrtc::kFreq16000Hz) {
+      channels_[channel]->cn16_type = type;
+    }
+    return 0;
+  }
+  WEBRTC_FUNC(GetRecPayloadType, (int channel, webrtc::CodecInst& codec)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    Channel* ch = channels_[channel];
+    for (std::vector<webrtc::CodecInst>::iterator it = ch->recv_codecs.begin();
+         it != ch->recv_codecs.end(); ++it) {
+      if (strcmp(it->plname, codec.plname) == 0 &&
+          it->plfreq == codec.plfreq &&
+          it->pltype != -1) {
+        codec.pltype = it->pltype;
+        return 0;
+      }
+    }
+    return -1;  // not found
+  }
+  WEBRTC_FUNC(SetVADStatus, (int channel, bool enable, webrtc::VadModes mode,
+                             bool disableDTX)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->vad = enable;
+    return 0;
+  }
+  WEBRTC_STUB(GetVADStatus, (int channel, bool& enabled,
+                             webrtc::VadModes& mode, bool& disabledDTX));
+
+  // webrtc::VoEDtmf
+  WEBRTC_STUB(SendTelephoneEvent, (int channel, int eventCode,
+      bool outOfBand = true, int lengthMs = 160, int attenuationDb = 10));
+
+  WEBRTC_FUNC(SetSendTelephoneEventPayloadType,
+      (int channel, unsigned char type)) {
+    channels_[channel]->dtmf_type = type;
+    return 0;
+  };
+  WEBRTC_STUB(GetSendTelephoneEventPayloadType,
+      (int channel, unsigned char& type));
+
+  WEBRTC_STUB(SetDtmfFeedbackStatus, (bool enable, bool directFeedback));
+  WEBRTC_STUB(GetDtmfFeedbackStatus, (bool& enabled, bool& directFeedback));
+  WEBRTC_STUB(RegisterTelephoneEventDetection, (int channel,
+      webrtc::TelephoneEventDetectionMethods detectionMethod,
+      webrtc::VoETelephoneEventObserver& observer));
+  WEBRTC_STUB(DeRegisterTelephoneEventDetection, (int channel));
+  WEBRTC_STUB(SetDtmfPlayoutStatus, (int channel, bool enable));
+  WEBRTC_STUB(GetDtmfPlayoutStatus, (int channel, bool& enabled));
+
+
+  WEBRTC_STUB(PlayDtmfTone,
+      (int eventCode, int lengthMs = 200, int attenuationDb = 10));
+  WEBRTC_STUB(StartPlayingDtmfTone,
+      (int eventCode, int attenuationDb = 10));
+  WEBRTC_STUB(StopPlayingDtmfTone, ());
+  WEBRTC_STUB(GetTelephoneEventDetectionStatus, (int channel,
+      bool& enabled, webrtc::TelephoneEventDetectionMethods& detectionMethod));
+
+  // webrtc::VoEFile
+  WEBRTC_FUNC(StartPlayingFileLocally, (int channel, const char* fileNameUTF8,
+                                        bool loop, webrtc::FileFormats format,
+                                        float volumeScaling, int startPointMs,
+                                        int stopPointMs)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->file = true;
+    return 0;
+  }
+  WEBRTC_FUNC(StartPlayingFileLocally, (int channel, webrtc::InStream* stream,
+                                        webrtc::FileFormats format,
+                                        float volumeScaling, int startPointMs,
+                                        int stopPointMs)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->file = true;
+    return 0;
+  }
+  WEBRTC_FUNC(StopPlayingFileLocally, (int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->file = false;
+    return 0;
+  }
+  WEBRTC_FUNC(IsPlayingFileLocally, (int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    return (channels_[channel]->file) ? 1 : 0;
+  }
+  WEBRTC_STUB(ScaleLocalFilePlayout, (int channel, float scale));
+  WEBRTC_STUB(StartPlayingFileAsMicrophone, (int channel,
+                                             const char* fileNameUTF8,
+                                             bool loop,
+                                             bool mixWithMicrophone,
+                                             webrtc::FileFormats format,
+                                             float volumeScaling));
+  WEBRTC_STUB(StartPlayingFileAsMicrophone, (int channel,
+                                             webrtc::InStream* stream,
+                                             bool mixWithMicrophone,
+                                             webrtc::FileFormats format,
+                                             float volumeScaling));
+  WEBRTC_STUB(StopPlayingFileAsMicrophone, (int channel));
+  WEBRTC_STUB(IsPlayingFileAsMicrophone, (int channel));
+  WEBRTC_STUB(ScaleFileAsMicrophonePlayout, (int channel, float scale));
+  WEBRTC_STUB(StartRecordingPlayout, (int channel, const char* fileNameUTF8,
+                                      webrtc::CodecInst* compression,
+                                      int maxSizeBytes));
+  WEBRTC_STUB(StartRecordingPlayout, (int channel, webrtc::OutStream* stream,
+                                      webrtc::CodecInst* compression));
+  WEBRTC_STUB(StopRecordingPlayout, (int channel));
+  WEBRTC_FUNC(StartRecordingMicrophone, (const char* fileNameUTF8,
+                                         webrtc::CodecInst* compression,
+                                         int maxSizeBytes)) {
+    if (fail_start_recording_microphone_) {
+      return -1;
+    }
+    recording_microphone_ = true;
+    return 0;
+  }
+  WEBRTC_FUNC(StartRecordingMicrophone, (webrtc::OutStream* stream,
+                                         webrtc::CodecInst* compression)) {
+    if (fail_start_recording_microphone_) {
+      return -1;
+    }
+    recording_microphone_ = true;
+    return 0;
+  }
+  WEBRTC_FUNC(StopRecordingMicrophone, ()) {
+    if (!recording_microphone_) {
+      return -1;
+    }
+    recording_microphone_ = false;
+    return 0;
+  }
+  WEBRTC_STUB(ConvertPCMToWAV, (const char* fileNameInUTF8,
+                                const char* fileNameOutUTF8));
+  WEBRTC_STUB(ConvertPCMToWAV, (webrtc::InStream* streamIn,
+                                webrtc::OutStream* streamOut));
+  WEBRTC_STUB(ConvertWAVToPCM, (const char* fileNameInUTF8,
+                                const char* fileNameOutUTF8));
+  WEBRTC_STUB(ConvertWAVToPCM, (webrtc::InStream* streamIn,
+                                webrtc::OutStream* streamOut));
+  WEBRTC_STUB(ConvertPCMToCompressed, (const char* fileNameInUTF8,
+                                       const char* fileNameOutUTF8,
+                                       webrtc::CodecInst* compression));
+  WEBRTC_STUB(ConvertPCMToCompressed, (webrtc::InStream* streamIn,
+                                       webrtc::OutStream* streamOut,
+                                       webrtc::CodecInst* compression));
+  WEBRTC_STUB(ConvertCompressedToPCM, (const char* fileNameInUTF8,
+                                     const char* fileNameOutUTF8));
+  WEBRTC_STUB(ConvertCompressedToPCM, (webrtc::InStream* streamIn,
+                                       webrtc::OutStream* streamOut));
+  WEBRTC_STUB(GetFileDuration, (const char* fileNameUTF8, int& durationMs,
+                                webrtc::FileFormats format));
+  WEBRTC_STUB(GetPlaybackPosition, (int channel, int& positionMs));
+
+  // webrtc::VoEHardware
+  WEBRTC_STUB(GetCPULoad, (int&));
+  WEBRTC_STUB(GetSystemCPULoad, (int&));
+  WEBRTC_FUNC(GetNumOfRecordingDevices, (int& num)) {
+    return GetNumDevices(num);
+  }
+  WEBRTC_FUNC(GetNumOfPlayoutDevices, (int& num)) {
+    return GetNumDevices(num);
+  }
+  WEBRTC_FUNC(GetRecordingDeviceName, (int i, char* name, char* guid)) {
+    return GetDeviceName(i, name, guid);
+  }
+  WEBRTC_FUNC(GetPlayoutDeviceName, (int i, char* name, char* guid)) {
+    return GetDeviceName(i, name, guid);
+  }
+  WEBRTC_STUB(SetRecordingDevice, (int, webrtc::StereoChannel));
+  WEBRTC_STUB(SetPlayoutDevice, (int));
+  WEBRTC_STUB(SetAudioDeviceLayer, (webrtc::AudioLayers));
+  WEBRTC_STUB(GetAudioDeviceLayer, (webrtc::AudioLayers&));
+  WEBRTC_STUB(GetPlayoutDeviceStatus, (bool&));
+  WEBRTC_STUB(GetRecordingDeviceStatus, (bool&));
+  WEBRTC_STUB(ResetAudioDevice, ());
+  WEBRTC_STUB(AudioDeviceControl, (unsigned int, unsigned int, unsigned int));
+  WEBRTC_STUB(NeedMorePlayData, (short int*, int, int, int, int&));
+  WEBRTC_STUB(RecordedDataIsAvailable, (short int*, int, int, int, int&));
+  WEBRTC_STUB(GetDevice, (char*, unsigned int));
+  WEBRTC_STUB(GetPlatform, (char*, unsigned int));
+  WEBRTC_STUB(GetOS, (char*, unsigned int));
+  WEBRTC_STUB(SetGrabPlayout, (bool));
+  WEBRTC_STUB(SetGrabRecording, (bool));
+  WEBRTC_STUB(SetLoudspeakerStatus, (bool enable));
+  WEBRTC_STUB(GetLoudspeakerStatus, (bool& enabled));
+  WEBRTC_STUB(EnableBuiltInAEC, (bool enable));
+  virtual bool BuiltInAECIsEnabled() const { return true; }
+  WEBRTC_STUB(SetSamplingRate, (int));
+  WEBRTC_STUB(GetSamplingRate, (int&));
+
+  // webrtc::VoENetEqStats
+  WEBRTC_STUB(GetNetworkStatistics, (int, webrtc::NetworkStatistics&));
+  WEBRTC_STUB(GetJitterStatistics, (int, webrtc::JitterStatistics&));
+  WEBRTC_STUB(GetPreferredBufferSize, (int, short unsigned int&));
+  WEBRTC_STUB(ResetJitterStatistics, (int));
+
+  // webrtc::VoENetwork
+  WEBRTC_FUNC(RegisterExternalTransport, (int channel,
+                                          webrtc::Transport& transport)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->external_transport = true;
+    return 0;
+  }
+  WEBRTC_FUNC(DeRegisterExternalTransport, (int channel)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->external_transport = false;
+    return 0;
+  }
+  WEBRTC_FUNC(ReceivedRTPPacket, (int channel, const void* data,
+                                  unsigned int length)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    if (!channels_[channel]->external_transport) return -1;
+    channels_[channel]->packets.push_back(
+        std::string(static_cast<const char*>(data), length));
+    return 0;
+  }
+  WEBRTC_STUB(ReceivedRTCPPacket, (int channel, const void* data,
+                                   unsigned int length));
+  WEBRTC_STUB(GetSourceInfo, (int channel, int& rtpPort, int& rtcpPort,
+                              char ipaddr[64]));
+  WEBRTC_STUB(GetLocalIP, (char ipaddr[64], bool ipv6));
+  WEBRTC_STUB(EnableIPv6, (int channel));
+  // Not using WEBRTC_STUB due to bool return value
+  virtual bool IPv6IsEnabled(int channel) { return true; }
+  WEBRTC_STUB(SetSourceFilter, (int channel, int rtpPort, int rtcpPort,
+                                const char ipaddr[64]));
+  WEBRTC_STUB(GetSourceFilter, (int channel, int& rtpPort, int& rtcpPort,
+                                char ipaddr[64]));
+  WEBRTC_STUB(SetSendTOS, (int channel, int priority,
+                           int DSCP, bool useSetSockopt));
+  WEBRTC_STUB(GetSendTOS, (int channel, int& priority,
+                           int& DSCP, bool& useSetSockopt));
+  WEBRTC_STUB(SetSendGQoS, (int channel, bool enable, int serviceType,
+                            int overrideDSCP));
+  WEBRTC_STUB(GetSendGQoS, (int channel, bool& enabled, int& serviceType,
+                            int& overrideDSCP));
+  WEBRTC_STUB(SetPacketTimeoutNotification, (int channel, bool enable,
+                                             int timeoutSeconds));
+  WEBRTC_STUB(GetPacketTimeoutNotification, (int channel, bool& enable,
+                                             int& timeoutSeconds));
+  WEBRTC_STUB(RegisterDeadOrAliveObserver, (int channel,
+      webrtc::VoEConnectionObserver& observer));
+  WEBRTC_STUB(DeRegisterDeadOrAliveObserver, (int channel));
+  WEBRTC_STUB(GetPeriodicDeadOrAliveStatus, (int channel, bool& enabled,
+                                             int& sampleTimeSeconds));
+  WEBRTC_STUB(SetPeriodicDeadOrAliveStatus, (int channel, bool enable,
+                                             int sampleTimeSeconds));
+  WEBRTC_STUB(SendUDPPacket, (int channel, const void* data,
+                              unsigned int length, int& transmittedBytes,
+                              bool useRtcpSocket));
+
+  // webrtc::VoERTP_RTCP
+  WEBRTC_STUB(RegisterRTPObserver, (int channel,
+                                    webrtc::VoERTPObserver& observer));
+  WEBRTC_STUB(DeRegisterRTPObserver, (int channel));
+  WEBRTC_STUB(RegisterRTCPObserver, (int channel,
+                                     webrtc::VoERTCPObserver& observer));
+  WEBRTC_STUB(DeRegisterRTCPObserver, (int channel));
+  WEBRTC_FUNC(SetLocalSSRC, (int channel, unsigned int ssrc)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->send_ssrc = ssrc;
+    return 0;
+  }
+  WEBRTC_FUNC(GetLocalSSRC, (int channel, unsigned int& ssrc)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    ssrc = channels_[channel]->send_ssrc;
+    return 0;
+  }
+  WEBRTC_STUB(GetRemoteSSRC, (int channel, unsigned int& ssrc));
+  WEBRTC_FUNC(SetRTPAudioLevelIndicationStatus, (int channel, bool enable,
+      unsigned char ID)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->level_header_ext_ = (enable) ? ID : -1;
+    return 0;
+  }
+  WEBRTC_FUNC(GetRTPAudioLevelIndicationStatus, (int channel, bool& enabled,
+      unsigned char& ID)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    enabled = (channels_[channel]->level_header_ext_ != -1);
+    ID = channels_[channel]->level_header_ext_;
+    return 0;
+  }
+  WEBRTC_STUB(GetRemoteCSRCs, (int channel, unsigned int arrCSRC[15]));
+  WEBRTC_STUB(GetRemoteEnergy, (int channel, unsigned char arrEnergy[15]));
+  WEBRTC_STUB(SetRTCPStatus, (int channel, bool enable));
+  WEBRTC_STUB(GetRTCPStatus, (int channel, bool& enabled));
+  WEBRTC_STUB(SetRTCP_CNAME, (int channel, const char cname[256]));
+  WEBRTC_STUB(GetRTCP_CNAME, (int channel, char cname[256]));
+  WEBRTC_STUB(GetRemoteRTCP_CNAME, (int channel, char* cname));
+  WEBRTC_STUB(GetRemoteRTCPData, (int channel, unsigned int& NTPHigh,
+                                  unsigned int& NTPLow,
+                                  unsigned int& timestamp,
+                                  unsigned int& playoutTimestamp,
+                                  unsigned int* jitter,
+                                  unsigned short* fractionLost));
+  WEBRTC_STUB(SendApplicationDefinedRTCPPacket, (int channel,
+                                                 const unsigned char subType,
+                                                 unsigned int name,
+                                                 const char* data,
+                                                 unsigned short dataLength));
+  WEBRTC_STUB(GetRTPStatistics, (int channel, unsigned int& averageJitterMs,
+                                 unsigned int& maxJitterMs,
+                                 unsigned int& discardedPackets));
+  WEBRTC_STUB(GetRTCPStatistics, (int channel, unsigned short& fractionLost,
+                                  unsigned int& cumulativeLost,
+                                  unsigned int& extendedMax,
+                                  unsigned int& jitterSamples, int& rttMs));
+  WEBRTC_STUB(GetRTCPStatistics, (int channel, webrtc::CallStatistics& stats));
+  WEBRTC_FUNC(SetFECStatus, (int channel, bool enable, int redPayloadtype)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    channels_[channel]->fec = enable;
+    channels_[channel]->fec_type = redPayloadtype;
+    return 0;
+  }
+  WEBRTC_FUNC(GetFECStatus, (int channel, bool& enable, int& redPayloadtype)) {
+    WEBRTC_CHECK_CHANNEL(channel);
+    enable = channels_[channel]->fec;
+    redPayloadtype = channels_[channel]->fec_type;
+    return 0;
+  }
+  WEBRTC_STUB(SetRTPKeepaliveStatus, (int channel, bool enable,
+                                      unsigned char unknownPayloadType,
+                                      int deltaTransmitTimeSeconds));
+  WEBRTC_STUB(GetRTPKeepaliveStatus, (int channel, bool& enabled,
+                                      unsigned char& unknownPayloadType,
+                                      int& deltaTransmitTimeSeconds));
+  WEBRTC_STUB(StartRTPDump, (int channel, const char* fileNameUTF8,
+                             webrtc::RTPDirections direction));
+  WEBRTC_STUB(StopRTPDump, (int channel, webrtc::RTPDirections direction));
+  WEBRTC_STUB(RTPDumpIsActive, (int channel, webrtc::RTPDirections direction));
+  WEBRTC_STUB(InsertExtraRTPPacket, (int channel, unsigned char payloadType,
+                                     bool markerBit, const char* payloadData,
+                                     unsigned short payloadSize));
+
+  // webrtc::VoEVideoSync
+  WEBRTC_STUB(GetPlayoutBufferSize, (int& bufferMs));
+  WEBRTC_STUB(GetPlayoutTimestamp, (int channel, unsigned int& timestamp));
+  WEBRTC_STUB(GetRtpRtcp, (int, webrtc::RtpRtcp*&));
+  WEBRTC_STUB(SetInitTimestamp, (int channel, unsigned int timestamp));
+  WEBRTC_STUB(SetInitSequenceNumber, (int channel, short sequenceNumber));
+  WEBRTC_STUB(SetMinimumPlayoutDelay, (int channel, int delayMs));
+  WEBRTC_STUB(GetDelayEstimate, (int channel, int& delayMs));
+  WEBRTC_STUB(GetSoundcardBufferSize, (int& bufferMs));
+
+  // webrtc::VoEVolumeControl
+  WEBRTC_STUB(SetSpeakerVolume, (unsigned int));
+  WEBRTC_STUB(GetSpeakerVolume, (unsigned int&));
+  WEBRTC_STUB(SetSystemOutputMute, (bool));
+  WEBRTC_STUB(GetSystemOutputMute, (bool&));
+  WEBRTC_STUB(SetMicVolume, (unsigned int));
+  WEBRTC_STUB(GetMicVolume, (unsigned int&));
+  WEBRTC_STUB(SetInputMute, (int, bool));
+  WEBRTC_STUB(GetInputMute, (int, bool&));
+  WEBRTC_STUB(SetSystemInputMute, (bool));
+  WEBRTC_STUB(GetSystemInputMute, (bool&));
+  WEBRTC_STUB(GetSpeechInputLevel, (unsigned int&));
+  WEBRTC_STUB(GetSpeechOutputLevel, (int, unsigned int&));
+  WEBRTC_STUB(GetSpeechInputLevelFullRange, (unsigned int&));
+  WEBRTC_STUB(GetSpeechOutputLevelFullRange, (int, unsigned int&));
+  WEBRTC_STUB(SetChannelOutputVolumeScaling, (int, float));
+  WEBRTC_STUB(GetChannelOutputVolumeScaling, (int, float&));
+  WEBRTC_STUB(SetOutputVolumePan, (int, float, float));
+  WEBRTC_STUB(GetOutputVolumePan, (int, float&, float&));
+
+  // webrtc::VoEAudioProcessing
+  WEBRTC_FUNC(SetNsStatus, (bool enable, webrtc::NsModes mode)) {
+    ns_enabled_ = enable;
+    ns_mode_ = mode;
+    return 0;
+  }
+  WEBRTC_FUNC(GetNsStatus, (bool& enabled, webrtc::NsModes& mode)) {
+    enabled = ns_enabled_;
+    mode = ns_mode_;
+    return 0;
+  }
+  WEBRTC_STUB(SetAgcStatus, (bool enable, webrtc::AgcModes mode));
+  WEBRTC_STUB(GetAgcStatus, (bool& enabled, webrtc::AgcModes& mode));
+
+  WEBRTC_FUNC(SetAgcConfig, (const webrtc::AgcConfig config)) {
+    agc_config_ = config;
+    return 0;
+  }
+  WEBRTC_FUNC(GetAgcConfig, (webrtc::AgcConfig& config)) {
+    config = agc_config_;
+    return 0;
+  }
+  WEBRTC_FUNC(SetEcStatus, (bool enable, webrtc::EcModes mode)) {
+    ec_enabled_ = enable;
+    ec_mode_ = mode;
+    return 0;
+  }
+  WEBRTC_FUNC(GetEcStatus, (bool& enabled, webrtc::EcModes& mode)) {
+    enabled = ec_enabled_;
+    mode = ec_mode_;
+    return 0;
+  }
+  WEBRTC_STUB(SetAecmMode, (webrtc::AecmModes mode, bool enableCNG));
+  WEBRTC_STUB(GetAecmMode, (webrtc::AecmModes& mode, bool& enabledCNG));
+  WEBRTC_STUB(SetRxNsStatus, (int channel, bool enable, webrtc::NsModes mode));
+  WEBRTC_STUB(GetRxNsStatus, (int channel, bool& enabled,
+                              webrtc::NsModes& mode));
+  WEBRTC_STUB(SetRxAgcStatus, (int channel, bool enable,
+                               webrtc::AgcModes mode));
+  WEBRTC_STUB(GetRxAgcStatus, (int channel, bool& enabled,
+                               webrtc::AgcModes& mode));
+  WEBRTC_STUB(SetRxAgcConfig, (int channel, const webrtc::AgcConfig config));
+  WEBRTC_STUB(GetRxAgcConfig, (int channel, webrtc::AgcConfig& config));
+
+  WEBRTC_STUB(RegisterRxVadObserver, (int, webrtc::VoERxVadCallback&));
+  WEBRTC_STUB(DeRegisterRxVadObserver, (int channel));
+  WEBRTC_STUB(VoiceActivityIndicator, (int channel));
+  WEBRTC_STUB(SetEcMetricsStatus, (bool enable));
+  WEBRTC_STUB(GetEcMetricsStatus, (bool& enable));
+  WEBRTC_STUB(GetEchoMetrics, (int& ERL, int& ERLE, int& RERL, int& A_NLP));
+  WEBRTC_STUB(GetEcDelayMetrics, (int& delay_median, int& delay_std));
+
+  WEBRTC_STUB(StartDebugRecording, (const char* fileNameUTF8));
+  WEBRTC_STUB(StopDebugRecording, ());
+
+  WEBRTC_STUB(SetTypingDetectionStatus, (bool enable));
+  WEBRTC_STUB(GetTypingDetectionStatus, (bool& enabled));
+
+  // webrtc::VoEExternalMedia
+  WEBRTC_FUNC(RegisterExternalMediaProcessing,
+              (int channel, webrtc::ProcessingTypes type,
+               webrtc::VoEMediaProcess& processObject)) {
+    media_processor_ = &processObject;
+    return 0;
+  }
+  WEBRTC_FUNC(DeRegisterExternalMediaProcessing,
+              (int channel, webrtc::ProcessingTypes type)) {
+    media_processor_ = NULL;
+    return 0;
+  }
+  WEBRTC_STUB(SetExternalRecordingStatus, (bool enable));
+  WEBRTC_STUB(SetExternalPlayoutStatus, (bool enable));
+  WEBRTC_STUB(ExternalRecordingInsertData,
+              (const WebRtc_Word16 speechData10ms[], int lengthSamples,
+               int samplingFreqHz, int current_delay_ms));
+  WEBRTC_STUB(ExternalPlayoutGetData,
+              (WebRtc_Word16 speechData10ms[], int samplingFreqHz,
+               int current_delay_ms, int& lengthSamples));
+
+ private:
+  int GetNumDevices(int& num) {
+#ifdef WIN32
+    num = 1;
+#else
+    // On non-Windows platforms VE adds a special entry for the default device,
+    // so if there is one physical device then there are two entries in the
+    // list.
+    num = 2;
+#endif
+    return 0;
+  }
+
+  int GetDeviceName(int i, char* name, char* guid) {
+    const char *s;
+#ifdef WIN32
+    if (0 == i) {
+      s = kFakeDeviceName;
+    } else {
+      return -1;
+    }
+#else
+    // See comment above.
+    if (0 == i) {
+      s = kFakeDefaultDeviceName;
+    } else if (1 == i) {
+      s = kFakeDeviceName;
+    } else {
+      return -1;
+    }
+#endif
+    strcpy(name, s);
+    guid[0] = '\0';
+    return 0;
+  }
+
+  bool inited_;
+  int last_channel_;
+  std::map<int, Channel*> channels_;
+  bool fail_create_channel_;
+  const cricket::AudioCodec* const* codecs_;
+  int num_codecs_;
+  bool ec_enabled_;
+  bool ns_enabled_;
+  webrtc::EcModes ec_mode_;
+  webrtc::NsModes ns_mode_;
+  webrtc::AgcConfig agc_config_;
+  webrtc::VoiceEngineObserver* observer_;
+  int playout_fail_channel_;
+  int send_fail_channel_;
+  bool fail_start_recording_microphone_;
+  bool recording_microphone_;
+  webrtc::VoEMediaProcess* media_processor_;
+};
+
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_FAKEWEBRTCVOICEENGINE_H_
diff --git a/talk/session/phone/videoadapter.cc b/talk/session/phone/videoadapter.cc
new file mode 100644
index 0000000..1cb1cc4
--- /dev/null
+++ b/talk/session/phone/videoadapter.cc
@@ -0,0 +1,496 @@
+// libjingle
+// Copyright 2010 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.
+
+#include "talk/session/phone/videoadapter.h"
+
+#include <limits.h>
+
+#include "talk/base/logging.h"
+#include "talk/base/timeutils.h"
+#include "talk/session/phone/videoframe.h"
+
+namespace cricket {
+
+// TODO: Make downgrades settable
+static const int kMaxCpuDowngrades = 2;  // Downgrade at most 2 times for CPU.
+static const int kDefaultDowngradeWaitTimeMs = 2000;
+
+// Cpu system load thresholds relative to max cpus.
+static const float kHighSystemThreshold = 0.95f;
+static const float kLowSystemThreshold = 0.75f;
+
+// Cpu process load thresholds relative to current cpus.
+static const float kMediumProcessThreshold = 0.50f;
+
+// TODO: Consider making scale factor table settable, to allow
+// application to select quality vs performance tradeoff.
+// List of scale factors that adapter will scale by.
+#if defined(IOS) || defined(ANDROID)
+// Mobile needs 1/4 scale for VGA (640x360) to QQVGA (160x90)
+// or 1/4 scale for HVGA (480x270) to QQHVGA (120x67)
+static const int kMinNumPixels = 120 * 67;
+static float kScaleFactors[] = {
+  1.f, // full size
+  3.f/4.f, // 3/4 scale
+  1.f/2.f, // 1/2 scale
+  3.f/8.f, // 3/8 scale
+  1.f/4.f, // 1/4 scale
+};
+#else
+// PC needs 1/8 scale for HD (1280x720) to QQVGA (160x90)
+static const int kMinNumPixels = 160 * 100;
+static float kScaleFactors[] = {
+  1.f, // full size
+  3.f/4.f, // 3/4 scale
+  1.f/2.f, // 1/2 scale
+  3.f/8.f, // 3/8 scale
+  1.f/4.f, // 1/4 scale
+  3.f/16.f, // 3/16 scale
+  1.f/8.f // 1/8 scale
+};
+#endif
+
+// Find scale factor that applied to width and height, is best match
+// to num_pixels.
+float VideoAdapter::FindClosestScale(int width, int height,
+                                     int target_num_pixels) {
+  if (!target_num_pixels) {
+    return 0.f;
+  }
+  int best_distance = INT_MAX;
+  int best_index = 0;  // default to unscaled
+  for (size_t i = 0u; i < ARRAY_SIZE(kScaleFactors); ++i) {
+    int test_num_pixels = static_cast<int>(width * kScaleFactors[i] *
+                                           height * kScaleFactors[i]);
+    int diff = test_num_pixels - target_num_pixels;
+    if (diff < 0) {
+      diff = -diff;
+    }
+    if (diff < best_distance) {
+      best_distance = diff;
+      best_index = i;
+      if (!best_distance) { // Found exact match
+        break;
+      }
+    }
+  }
+  return kScaleFactors[best_index];
+}
+
+// There are several frame sizes used by Adapter.  This explains them
+// input_format - set once by server to frame size expected from the camera.
+// output_format - size that output would like to be.  Includes framerate.
+// output_num_pixels - size that output should be constrained to.  Used to
+//   compute output_format from in_frame.
+// in_frame - actual camera captured frame size, which is typically the same
+//   as input_format.  This can also be rotated or cropped for aspect ratio.
+// out_frame - actual frame output by adapter.  Should be a direct scale of
+//   in_frame maintaining rotation and aspect ratio.
+// OnOutputFormatRequest - server requests you send this resolution based on
+//   view requests.
+// OnEncoderResolutionRequest - encoder requests you send this resolution based
+//   on bandwidth
+// OnCpuLoadUpdated - cpu monitor requests you send this resolution based on
+//   cpu load.
+
+///////////////////////////////////////////////////////////////////////
+// Implementation of VideoAdapter
+VideoAdapter::VideoAdapter()
+    : black_output_(false),
+      is_black_(false),
+      drop_frame_count_(0) {
+}
+
+VideoAdapter::~VideoAdapter() {
+}
+
+// TODO: Consider SetInputFormat and SetOutputFormat without
+// VideoFormat.
+void VideoAdapter::SetInputFormat(const VideoFormat& format) {
+  talk_base::CritScope cs(&critical_section_);
+  input_format_ = format;
+  output_format_.interval = talk_base::_max(
+      output_format_.interval, input_format_.interval);
+}
+
+void VideoAdapter::SetOutputFormat(const VideoFormat& format) {
+  talk_base::CritScope cs(&critical_section_);
+  output_format_ = format;
+  output_num_pixels_ = output_format_.width * output_format_.height;
+  output_format_.interval = talk_base::_max(
+      output_format_.interval, input_format_.interval);
+  drop_frame_count_ = 0;
+}
+
+const VideoFormat& VideoAdapter::input_format() {
+  talk_base::CritScope cs(&critical_section_);
+  return input_format_;
+}
+
+const VideoFormat& VideoAdapter::output_format() {
+  talk_base::CritScope cs(&critical_section_);
+  return output_format_;
+}
+
+void VideoAdapter::SetBlackOutput(bool black) {
+  talk_base::CritScope cs(&critical_section_);
+  black_output_ = black;
+}
+
+// Constrain output resolution to this many pixels overall
+void VideoAdapter::SetOutputNumPixels(int num_pixels) {
+  output_num_pixels_ = num_pixels;
+}
+
+int VideoAdapter::GetOutputNumPixels() const {
+  return output_num_pixels_;
+}
+
+bool VideoAdapter::AdaptFrame(const VideoFrame* in_frame,
+                              const VideoFrame** out_frame) {
+  talk_base::CritScope cs(&critical_section_);
+
+  if (!in_frame || !out_frame || input_format_.IsSize0x0()) {
+    return false;
+  }
+
+  // Drop the input frame if necessary.
+  bool should_drop = false;
+  if (!output_num_pixels_) {
+    // Drop all frames as the output format is 0x0.
+    should_drop = true;
+  } else {
+    // Drop some frames based on the ratio of the input fps and the output fps.
+    // We assume that the output fps is a factor of the input fps. In other
+    // words, the output interval is divided by the input interval evenly.
+    should_drop = (drop_frame_count_ > 0);
+    if (input_format_.interval > 0 &&
+        output_format_.interval > input_format_.interval) {
+      ++drop_frame_count_;
+      drop_frame_count_ %= output_format_.interval / input_format_.interval;
+    }
+  }
+
+  if (output_num_pixels_) {
+    float scale = VideoAdapter::FindClosestScale(in_frame->GetWidth(),
+                                                 in_frame->GetHeight(),
+                                                 output_num_pixels_);
+    output_format_.width = static_cast<int>(in_frame->GetWidth() * scale);
+    output_format_.height = static_cast<int>(in_frame->GetHeight() * scale);
+  }
+
+  if (should_drop) {
+    *out_frame = NULL;
+    return true;
+  }
+
+  if (!StretchToOutputFrame(in_frame)) {
+    return false;
+  }
+
+  *out_frame = output_frame_.get();
+  return true;
+}
+
+bool VideoAdapter::StretchToOutputFrame(const VideoFrame* in_frame) {
+  int output_width = output_format_.width;
+  int output_height = output_format_.height;
+
+  // Create and stretch the output frame if it has not been created yet or its
+  // size is not same as the expected.
+  bool stretched = false;
+  if (!output_frame_.get() ||
+      output_frame_->GetWidth() != static_cast<size_t>(output_width) ||
+      output_frame_->GetHeight() != static_cast<size_t>(output_height)) {
+    output_frame_.reset(
+        in_frame->Stretch(output_width, output_height, true, true));
+    if (!output_frame_.get()) {
+      LOG(LS_WARNING) << "Adapter failed to stretch frame to "
+                      << output_width << "x" << output_height;
+      return false;
+    }
+    stretched = true;
+    is_black_ = false;
+  }
+
+  if (!black_output_) {
+    if (!stretched) {
+      // The output frame does not need to be blacken and has not been stretched
+      // from the input frame yet, stretch the input frame. This is the most
+      // common case.
+      in_frame->StretchToFrame(output_frame_.get(), true, true);
+    }
+    is_black_ = false;
+  } else {
+    if (!is_black_) {
+      output_frame_->SetToBlack();
+      is_black_ = true;
+    }
+    output_frame_->SetElapsedTime(in_frame->GetElapsedTime());
+    output_frame_->SetTimeStamp(in_frame->GetTimeStamp());
+  }
+
+  return true;
+}
+
+///////////////////////////////////////////////////////////////////////
+// Implementation of CoordinatedVideoAdapter
+CoordinatedVideoAdapter::CoordinatedVideoAdapter()
+    : cpu_adaptation_(false),
+      gd_adaptation_(true),
+      view_adaptation_(true),
+      cpu_downgrade_count_(0),
+      cpu_downgrade_wait_time_(0),
+      view_desired_num_pixels_(INT_MAX),
+      view_desired_interval_(0),
+      encoder_desired_num_pixels_(INT_MAX),
+      cpu_desired_num_pixels_(INT_MAX) {
+}
+
+// Helper function to UPGRADE or DOWNGRADE a number of pixels
+void CoordinatedVideoAdapter::StepPixelCount(
+    CoordinatedVideoAdapter::AdaptRequest request,
+    int* num_pixels) {
+  switch (request) {
+    case CoordinatedVideoAdapter::DOWNGRADE:
+      *num_pixels /= 2;
+      break;
+
+    case CoordinatedVideoAdapter::UPGRADE:
+      *num_pixels *= 2;
+      break;
+
+    default:  // No change in pixel count
+      break;
+  }
+  return;
+}
+
+// Find the adaptation request of the cpu based on the load. Return UPGRADE if
+// the load is low, DOWNGRADE if the load is high, and KEEP otherwise.
+CoordinatedVideoAdapter::AdaptRequest CoordinatedVideoAdapter::FindCpuRequest(
+    int current_cpus, int max_cpus,
+    float process_load, float system_load) {
+  // Downgrade if system is high and plugin is at least more than midrange.
+  if (system_load >= kHighSystemThreshold * max_cpus &&
+      process_load >= kMediumProcessThreshold * current_cpus) {
+    return CoordinatedVideoAdapter::DOWNGRADE;
+  // Upgrade if system is low.
+  } else if (system_load < kLowSystemThreshold * max_cpus) {
+    return CoordinatedVideoAdapter::UPGRADE;
+  }
+  return CoordinatedVideoAdapter::KEEP;
+}
+
+// A remote view request for a new resolution.
+void CoordinatedVideoAdapter::OnOutputFormatRequest(const VideoFormat& format) {
+  talk_base::CritScope cs(&request_critical_section_);
+  if (!view_adaptation_) {
+    return;
+  }
+  // Set output for initial aspect ratio in mediachannel unittests.
+  int old_num_pixels = GetOutputNumPixels();
+  SetOutputFormat(format);
+  SetOutputNumPixels(old_num_pixels);
+  view_desired_num_pixels_ = format.width * format.height;
+  view_desired_interval_ = format.interval;
+  bool changed = AdaptToMinimumFormat();
+  LOG(LS_INFO) << "VAdapt View Request: "
+               << format.width << "x" << format.height
+               << " Pixels: " << view_desired_num_pixels_
+               << " Changed: " << (changed ? "true" : "false");
+}
+
+// A Bandwidth GD request for new resolution
+void CoordinatedVideoAdapter::OnEncoderResolutionRequest(
+    int width, int height, AdaptRequest request) {
+  talk_base::CritScope cs(&request_critical_section_);
+  if (!gd_adaptation_) {
+    return;
+  }
+  if (KEEP != request) {
+    int new_encoder_desired_num_pixels = width * height;
+    int old_num_pixels = GetOutputNumPixels();
+    if (new_encoder_desired_num_pixels != old_num_pixels) {
+      LOG(LS_VERBOSE) << "VAdapt GD resolution stale.  Ignored";
+    } else {
+      // Update the encoder desired format based on the request.
+      encoder_desired_num_pixels_ = new_encoder_desired_num_pixels;
+      StepPixelCount(request, &encoder_desired_num_pixels_);
+    }
+  }
+  bool changed = AdaptToMinimumFormat();
+  LOG(LS_INFO) << "VAdapt GD Request: "
+               << (DOWNGRADE == request ? "down" :
+                   (UPGRADE == request ? "up" : "keep"))
+               << " From: " << width << "x" << height
+               << " Pixels: " << encoder_desired_num_pixels_
+               << " Changed: " << (changed ? "true" : "false");
+}
+
+// A CPU request for new resolution
+void CoordinatedVideoAdapter::OnCpuLoadUpdated(
+    int current_cpus, int max_cpus, float process_load, float system_load) {
+  talk_base::CritScope cs(&request_critical_section_);
+  if (!cpu_adaptation_) {
+    return;
+  }
+  AdaptRequest request = FindCpuRequest(current_cpus, max_cpus,
+                                        process_load, system_load);
+  // Update how many times we have downgraded due to the cpu load.
+  switch (request) {
+    case DOWNGRADE:
+      if (cpu_downgrade_count_ < kMaxCpuDowngrades) {
+        // Ignore downgrades if we have downgraded the maximum times or we just
+        // downgraded in a short time.
+        if (cpu_downgrade_wait_time_ != 0 &&
+            talk_base::TimeIsLater(talk_base::Time(),
+                                   cpu_downgrade_wait_time_)) {
+          LOG(LS_VERBOSE) << "VAdapt CPU load high but do not downgrade until "
+                          << talk_base::TimeUntil(cpu_downgrade_wait_time_)
+                          << " ms.";
+          request = KEEP;
+        } else {
+          ++cpu_downgrade_count_;
+        }
+      } else {
+          LOG(LS_VERBOSE) << "VAdapt CPU load high but do not downgrade "
+                             "because maximum downgrades reached";
+      }
+      break;
+    case UPGRADE:
+      if (cpu_downgrade_count_ > 0) {
+        bool is_min = IsMinimumFormat(cpu_desired_num_pixels_);
+        if (is_min) {
+          --cpu_downgrade_count_;
+        } else {
+         LOG(LS_VERBOSE) << "VAdapt CPU load low but do not upgrade "
+                             "because cpu is not limiting resolution";
+        }
+      } else {
+          LOG(LS_VERBOSE) << "VAdapt CPU load low but do not upgrade "
+                             "because minimum downgrades reached";
+      }
+      break;
+    case KEEP:
+    default:
+      break;
+  }
+  if (KEEP != request) {
+    // TODO: compute stepping up/down from OutputNumPixels but
+    // clamp to inputpixels / 4 (2 steps)
+    cpu_desired_num_pixels_ = static_cast<int>(
+        input_format().width * input_format().height >> cpu_downgrade_count_);
+  }
+  bool changed = AdaptToMinimumFormat();
+  LOG(LS_INFO) << "VAdapt CPU Request: "
+               << (DOWNGRADE == request ? "down" :
+                   (UPGRADE == request ? "up" : "keep"))
+               << " Process: " << process_load
+               << " System: " << system_load
+               << " Steps: " << cpu_downgrade_count_
+               << " Changed: " << (changed ? "true" : "false");
+}
+
+// Called by cpu adapter on up requests.
+bool CoordinatedVideoAdapter::IsMinimumFormat(int pixels) {
+  // Find closest scale factor that matches input resolution to min_num_pixels
+  // and set that for output resolution.  This is not needed for VideoAdapter,
+  // but provides feedback to unittests and users on expected resolution.
+  // Actual resolution is based on input frame.
+  VideoFormat new_output = output_format();
+  VideoFormat input = input_format();
+  if (input_format().IsSize0x0()) {
+    input = new_output;
+  }
+  float scale = 1.0f;
+  if (!input.IsSize0x0()) {
+    scale = FindClosestScale(input.width,
+                             input.height,
+                             pixels);
+  }
+  new_output.width = static_cast<int>(input.width * scale);
+  new_output.height = static_cast<int>(input.height * scale);
+  int new_pixels = new_output.width * new_output.height;
+  int num_pixels = GetOutputNumPixels();
+  return new_pixels <= num_pixels;
+}
+
+// Called by all coordinators when there is a change.
+bool CoordinatedVideoAdapter::AdaptToMinimumFormat() {
+  int old_num_pixels = GetOutputNumPixels();
+  // Get the min of the formats that the server, encoder, and cpu wants.
+  int min_num_pixels = view_desired_num_pixels_;
+  if (encoder_desired_num_pixels_ &&
+      (encoder_desired_num_pixels_ < min_num_pixels)) {
+    min_num_pixels = encoder_desired_num_pixels_;
+  }
+  if (cpu_adaptation_ && cpu_desired_num_pixels_ &&
+      (cpu_desired_num_pixels_ < min_num_pixels)) {
+    min_num_pixels = cpu_desired_num_pixels_;
+    // Update the cpu_downgrade_wait_time_ if we are going to downgrade video.
+    cpu_downgrade_wait_time_ =
+      talk_base::TimeAfter(kDefaultDowngradeWaitTimeMs);
+  }
+  // prevent going below QQVGA
+  if (min_num_pixels > 0 && min_num_pixels < kMinNumPixels) {
+    min_num_pixels = kMinNumPixels;
+  }
+  SetOutputNumPixels(min_num_pixels);
+
+  // Find closest scale factor that matches input resolution to min_num_pixels
+  // and set that for output resolution.  This is not needed for VideoAdapter,
+  // but provides feedback to unittests and users on expected resolution.
+  // Actual resolution is based on input frame.
+  VideoFormat new_output = output_format();
+  VideoFormat input = input_format();
+  if (input_format().IsSize0x0()) {
+    input = new_output;
+  }
+  float scale = 1.0f;
+  if (!input.IsSize0x0()) {
+    scale = FindClosestScale(input.width,
+                             input.height,
+                             min_num_pixels);
+  }
+  new_output.width = static_cast<int>(input.width * scale);
+  new_output.height = static_cast<int>(input.height * scale);
+  new_output.interval = view_desired_interval_;
+  SetOutputFormat(new_output);
+  int new_num_pixels = GetOutputNumPixels();
+  bool changed = new_num_pixels != old_num_pixels;
+
+  LOG(LS_VERBOSE) << "VAdapt Status View: " << view_desired_num_pixels_
+                  << " GD: " << encoder_desired_num_pixels_
+                  << " CPU: " << cpu_desired_num_pixels_
+                  << " Pixels: " << min_num_pixels
+                  << " Scale: " << scale
+                  << " Resolution: " << new_output.width
+                  << "x" << new_output.height
+                  << " Changed: " << (changed ? "true" : "false");
+  return changed;
+}
+
+}  // namespace cricket
diff --git a/talk/session/phone/videoadapter.h b/talk/session/phone/videoadapter.h
new file mode 100644
index 0000000..6c14255
--- /dev/null
+++ b/talk/session/phone/videoadapter.h
@@ -0,0 +1,145 @@
+// libjingle
+// Copyright 2010 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.
+
+#ifndef TALK_SESSION_PHONE_VIDEOADAPTER_H_
+#define TALK_SESSION_PHONE_VIDEOADAPTER_H_
+
+#include "talk/base/criticalsection.h"
+#include "talk/base/scoped_ptr.h"
+#include "talk/base/sigslot.h"
+#include "talk/session/phone/videocommon.h"
+
+namespace cricket {
+
+class VideoFrame;
+
+// VideoAdapter adapts an input video frame to an output frame based on the
+// specified input and output formats. The adaptation includes dropping frames
+// to reduce frame rate and scaling frames. VideoAdapter is thread safe.
+class VideoAdapter {
+ public:
+  VideoAdapter();
+  virtual ~VideoAdapter();
+
+  void SetInputFormat(const VideoFormat& format);
+  void SetOutputFormat(const VideoFormat& format);
+  // Constrain output resolution to this many pixels overall
+  void SetOutputNumPixels(int num_pixels);
+  int GetOutputNumPixels() const;
+
+  const VideoFormat& input_format();
+  const VideoFormat& output_format();
+  // If the parameter black is true, the adapted frames will be black.
+  void SetBlackOutput(bool black);
+
+  // Adapt the input frame from the input format to the output format. Return
+  // true and set the output frame to NULL if the input frame is dropped. Return
+  // true and set the out frame to output_frame_ if the input frame is adapted
+  // successfully. Return false otherwise.
+  // output_frame_ is owned by the VideoAdapter that has the best knowledge on
+  // the output frame.
+  bool AdaptFrame(const VideoFrame* in_frame, const VideoFrame** out_frame);
+
+ protected:
+  float FindClosestScale(int width, int height, int target_num_pixels);
+
+ private:
+  bool StretchToOutputFrame(const VideoFrame* in_frame);
+
+  VideoFormat input_format_;
+  VideoFormat output_format_;
+  int output_num_pixels_;
+  bool black_output_;  // Flag to tell if we need to black output_frame_.
+  bool is_black_;  // Flag to tell if output_frame_ is currently black.
+  int64 drop_frame_count_;
+  talk_base::scoped_ptr<VideoFrame> output_frame_;
+  // The critical section to protect the above variables.
+  talk_base::CriticalSection critical_section_;
+
+  DISALLOW_COPY_AND_ASSIGN(VideoAdapter);
+};
+
+// CoordinatedVideoAdapter adapts the video input to the encoder by coordinating
+// the format request from the server, the resolution request from the encoder,
+// and the CPU load.
+class CoordinatedVideoAdapter
+    : public VideoAdapter, public sigslot::has_slots<>  {
+ public:
+  enum AdaptRequest { UPGRADE, KEEP, DOWNGRADE };
+
+  CoordinatedVideoAdapter();
+  virtual ~CoordinatedVideoAdapter() {}
+
+  // Enable or disable video adaptation due to the change of the CPU load.
+  void set_cpu_adaptation(bool enable) { cpu_adaptation_ = enable; }
+  bool cpu_adaptation() const { return cpu_adaptation_; }
+  // Enable or disable video adaptation due to the change of the GD
+  void set_gd_adaptation(bool enable) { gd_adaptation_ = enable; }
+  bool gd_adaptation() const { return gd_adaptation_; }
+  // Enable or disable video adaptation due to the change of the View
+  void set_view_adaptation(bool enable) { view_adaptation_ = enable; }
+  bool view_adaptation() const { return view_adaptation_; }
+  // When the video is decreased, set the waiting time for CPU adaptation to
+  // decrease video again.
+  void set_cpu_downgrade_wait_time(uint32 ms) { cpu_downgrade_wait_time_ = ms; }
+  // Handle the format request from the server via Jingle update message.
+  void OnOutputFormatRequest(const VideoFormat& format);
+  // Handle the resolution request from the encoder due to bandwidth changes.
+  void OnEncoderResolutionRequest(int width, int height, AdaptRequest request);
+  // Handle the CPU load provided by a CPU monitor.
+  void OnCpuLoadUpdated(int current_cpus, int max_cpus,
+                        float process_load, float system_load);
+
+ private:
+  // Adapt to the minimum of the formats the server requests, the CPU wants, and
+  // the encoder wants.  Returns true if resolution changed.
+  bool AdaptToMinimumFormat();
+  bool IsMinimumFormat(int pixels);
+  void StepPixelCount(CoordinatedVideoAdapter::AdaptRequest request,
+                      int* num_pixels);
+  CoordinatedVideoAdapter::AdaptRequest FindCpuRequest(
+    int current_cpus, int max_cpus,
+    float process_load, float system_load);
+
+  bool cpu_adaptation_;  // True if cpu adaptation is enabled.
+  bool gd_adaptation_;  // True if gd adaptation is enabled.
+  bool view_adaptation_;  // True if view adaptation is enabled.
+  int cpu_downgrade_count_;
+  int cpu_downgrade_wait_time_;
+  // Video formats that the server view requests, the CPU wants, and the encoder
+  // wants respectively. The adapted output format is the minimum of these.
+  int view_desired_num_pixels_;
+  int64 view_desired_interval_;
+  int encoder_desired_num_pixels_;
+  int cpu_desired_num_pixels_;
+  // The critical section to protect handling requests.
+  talk_base::CriticalSection request_critical_section_;
+
+  DISALLOW_COPY_AND_ASSIGN(CoordinatedVideoAdapter);
+};
+
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_VIDEOADAPTER_H_
diff --git a/talk/session/phone/videocommon.h b/talk/session/phone/videocommon.h
index fb1f393..6662bfc 100644
--- a/talk/session/phone/videocommon.h
+++ b/talk/session/phone/videocommon.h
@@ -117,35 +117,31 @@
 struct VideoFormatPod {
   int width;  // in number of pixels
   int height;  // in number of pixels
-  int framerate;
+  int64 interval;  // in nanoseconds
   uint32 fourcc;  // color space. FOURCC_ANY means that any color space is OK.
 };
 
-struct VideoFormat {
+struct VideoFormat : VideoFormatPod{
   static const int64 kMinimumInterval =
       talk_base::kNumNanosecsPerSec / 10000;  // 10k fps
 
-  VideoFormat() : width(0), height(0), interval(0), fourcc(0) {}
-
-  VideoFormat(int w, int h, int64 interval_ns, uint32 cc)
-      : width(w),
-        height(h),
-        interval(interval_ns),
-        fourcc(cc) {
+  VideoFormat() {
+    Construct(0, 0, 0, 0);
   }
 
-  VideoFormat(const VideoFormat& format)
-      : width(format.width),
-        height(format.height),
-        interval(format.interval),
-        fourcc(format.fourcc) {
+  VideoFormat(int w, int h, int64 interval_ns, uint32 cc) {
+    Construct(w, h, interval_ns, cc);
   }
 
-  explicit VideoFormat(const VideoFormatPod& format)
-      : width(format.width),
-        height(format.height),
-        interval(FpsToInterval(format.framerate)),
-        fourcc(format.fourcc) {
+  explicit VideoFormat(const VideoFormatPod& format) {
+    Construct(format.width, format.height, format.interval, format.fourcc);
+  }
+
+  void Construct(int w, int h, int64 interval_ns, uint32 cc) {
+    width = w;
+    height = h;
+    interval = interval_ns;
+    fourcc = cc;
   }
 
   static int64 FpsToInterval(int fps) {
@@ -190,11 +186,6 @@
 
   // Get a string presentation in the form of "fourcc width x height x fps"
   std::string ToString() const;
-
-  int    width;     // in number of pixels
-  int    height;    // in number of pixels
-  int64  interval;  // in nanoseconds
-  uint32 fourcc;    // color space. FOURCC_ANY means that any color space is OK.
 };
 
 // Result of video capturer start.
diff --git a/talk/session/phone/webrtcvideoengine.cc b/talk/session/phone/webrtcvideoengine.cc
index 046ae5f..5b110d5 100644
--- a/talk/session/phone/webrtcvideoengine.cc
+++ b/talk/session/phone/webrtcvideoengine.cc
@@ -232,32 +232,34 @@
 #endif
 };
 
+static const int64 kNsPerFrame = 33333333;  // 30fps
+
 // The formats are sorted by the descending order of width. We use the order to
 // find the next format for CPU and bandwidth adaptation.
 const VideoFormatPod WebRtcVideoEngine::kVideoFormats[] = {
-  {1280, 800, 30, FOURCC_ANY},
-  {1280, 720, 30, FOURCC_ANY},
-  {960, 600, 30, FOURCC_ANY},
-  {960, 540, 30, FOURCC_ANY},
-  {640, 400, 30, FOURCC_ANY},
-  {640, 360, 30, FOURCC_ANY},
-  {640, 480, 30, FOURCC_ANY},
-  {480, 300, 30, FOURCC_ANY},
-  {480, 270, 30, FOURCC_ANY},
-  {480, 360, 30, FOURCC_ANY},
-  {320, 200, 30, FOURCC_ANY},
-  {320, 180, 30, FOURCC_ANY},
-  {320, 240, 30, FOURCC_ANY},
-  {240, 150, 30, FOURCC_ANY},
-  {240, 135, 30, FOURCC_ANY},
-  {240, 180, 30, FOURCC_ANY},
-  {160, 100, 30, FOURCC_ANY},
-  {160, 90, 30, FOURCC_ANY},
-  {160, 120, 30, FOURCC_ANY},
+  {1280, 800, kNsPerFrame, FOURCC_ANY},
+  {1280, 720, kNsPerFrame, FOURCC_ANY},
+  {960, 600, kNsPerFrame, FOURCC_ANY},
+  {960, 540, kNsPerFrame, FOURCC_ANY},
+  {640, 400, kNsPerFrame, FOURCC_ANY},
+  {640, 360, kNsPerFrame, FOURCC_ANY},
+  {640, 480, kNsPerFrame, FOURCC_ANY},
+  {480, 300, kNsPerFrame, FOURCC_ANY},
+  {480, 270, kNsPerFrame, FOURCC_ANY},
+  {480, 360, kNsPerFrame, FOURCC_ANY},
+  {320, 200, kNsPerFrame, FOURCC_ANY},
+  {320, 180, kNsPerFrame, FOURCC_ANY},
+  {320, 240, kNsPerFrame, FOURCC_ANY},
+  {240, 150, kNsPerFrame, FOURCC_ANY},
+  {240, 135, kNsPerFrame, FOURCC_ANY},
+  {240, 180, kNsPerFrame, FOURCC_ANY},
+  {160, 100, kNsPerFrame, FOURCC_ANY},
+  {160, 90, kNsPerFrame, FOURCC_ANY},
+  {160, 120, kNsPerFrame, FOURCC_ANY},
 };
 
 const VideoFormatPod WebRtcVideoEngine::kDefaultVideoFormat =
-    {640, 400, 30, FOURCC_ANY};
+    {640, 400, kNsPerFrame, FOURCC_ANY};
 
 WebRtcVideoEngine::WebRtcVideoEngine() {
   Construct(new ViEWrapper(), new ViETraceWrapper(), NULL);
@@ -304,7 +306,7 @@
                        kVideoCodecPrefs[0].name,
                        kDefaultVideoFormat.width,
                        kDefaultVideoFormat.height,
-                       kDefaultVideoFormat.framerate,
+                       VideoFormat::IntervalToFps(kDefaultVideoFormat.interval),
                        0);
   if (!SetDefaultCodec(max_codec)) {
     LOG(LS_ERROR) << "Failed to initialize list of supported codec types";
diff --git a/talk/session/tunnel/tunnelsessionclient_unittest.cc b/talk/session/tunnel/tunnelsessionclient_unittest.cc
new file mode 100644
index 0000000..7ead1bf
--- /dev/null
+++ b/talk/session/tunnel/tunnelsessionclient_unittest.cc
@@ -0,0 +1,235 @@
+/*
+ * libjingle
+ * Copyright 2010, 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.
+ */
+
+#include <string>
+#include "talk/base/gunit.h"
+#include "talk/base/messagehandler.h"
+#include "talk/base/scoped_ptr.h"
+#include "talk/base/stream.h"
+#include "talk/base/thread.h"
+#include "talk/base/timeutils.h"
+#include "talk/p2p/base/sessionmanager.h"
+#include "talk/p2p/base/transport.h"
+#include "talk/p2p/client/fakeportallocator.h"
+#include "talk/session/tunnel/tunnelsessionclient.h"
+
+static const int kTimeoutMs = 10000;
+static const int kBlockSize = 4096;
+static const buzz::Jid kLocalJid("local@localhost");
+static const buzz::Jid kRemoteJid("remote@localhost");
+
+// This test fixture creates the necessary plumbing to create and run
+// two TunnelSessionClients that talk to each other.
+class TunnelSessionClientTest : public testing::Test,
+                                public talk_base::MessageHandler,
+                                public sigslot::has_slots<> {
+ public:
+  TunnelSessionClientTest()
+      : local_pa_(talk_base::Thread::Current(), NULL),
+        remote_pa_(talk_base::Thread::Current(), NULL),
+        local_sm_(&local_pa_, talk_base::Thread::Current()),
+        remote_sm_(&remote_pa_, talk_base::Thread::Current()),
+        local_client_(kLocalJid, &local_sm_),
+        remote_client_(kRemoteJid, &remote_sm_),
+        done_(false) {
+    local_sm_.SignalSessionCreate.connect(this,
+        &TunnelSessionClientTest::OnSessionCreate);
+    local_sm_.SignalRequestSignaling.connect(this,
+        &TunnelSessionClientTest::OnLocalRequestSignaling);
+    local_sm_.SignalOutgoingMessage.connect(this,
+        &TunnelSessionClientTest::OnOutgoingMessage);
+    remote_sm_.SignalSessionCreate.connect(this,
+        &TunnelSessionClientTest::OnSessionCreate);
+    remote_sm_.SignalRequestSignaling.connect(this,
+        &TunnelSessionClientTest::OnRemoteRequestSignaling);
+    remote_sm_.SignalOutgoingMessage.connect(this,
+        &TunnelSessionClientTest::OnOutgoingMessage);
+    remote_client_.SignalIncomingTunnel.connect(this,
+        &TunnelSessionClientTest::OnIncomingTunnel);
+  }
+
+  // Transfer the desired amount of data from the local to the remote client.
+  void TestTransfer(int size) {
+    // Create some dummy data to send.
+    send_stream_.ReserveSize(size);
+    for (int i = 0; i < size; ++i) {
+      char ch = static_cast<char>(i);
+      send_stream_.Write(&ch, 1, NULL, NULL);
+    }
+    send_stream_.Rewind();
+    // Prepare the receive stream.
+    recv_stream_.ReserveSize(size);
+    // Create the tunnel and set things in motion.
+    local_tunnel_.reset(local_client_.CreateTunnel(kRemoteJid, "test"));
+    local_tunnel_->SignalEvent.connect(this,
+        &TunnelSessionClientTest::OnStreamEvent);
+    EXPECT_TRUE_WAIT(done_, kTimeoutMs);
+    // Make sure we received the right data.
+    EXPECT_EQ(0, memcmp(send_stream_.GetBuffer(),
+                        recv_stream_.GetBuffer(), size));
+  }
+
+ private:
+  enum { MSG_LSIGNAL, MSG_RSIGNAL };
+
+  // Use this callback to allow local ips for this test.
+  void OnSessionCreate(cricket::Session* session, bool incoming) {
+    session->set_allow_local_ips(true);
+  }
+
+  // There's no SessionManager* argument in this callback, so we need 2 of them.
+  void OnLocalRequestSignaling() {
+    local_sm_.OnSignalingReady();
+  }
+  void OnRemoteRequestSignaling() {
+    remote_sm_.OnSignalingReady();
+  }
+
+  // Post a message, to avoid problems with directly connecting the callbacks.
+  void OnOutgoingMessage(cricket::SessionManager* manager,
+                         const buzz::XmlElement* stanza) {
+    if (manager == &local_sm_) {
+      talk_base::Thread::Current()->Post(this, MSG_LSIGNAL,
+          talk_base::WrapMessageData(*stanza));
+    } else if (manager == &remote_sm_) {
+      talk_base::Thread::Current()->Post(this, MSG_RSIGNAL,
+          talk_base::WrapMessageData(*stanza));
+    }
+  }
+
+  // Need to add a "from=" attribute (normally added by the server)
+  // Then route the incoming signaling message to the "other" session manager.
+  virtual void OnMessage(talk_base::Message* message) {
+    talk_base::TypedMessageData<buzz::XmlElement>* data =
+        static_cast<talk_base::TypedMessageData<buzz::XmlElement>*>(
+            message->pdata);
+    bool response = data->data().Attr(buzz::QN_TYPE) == buzz::STR_RESULT;
+    if (message->message_id == MSG_RSIGNAL) {
+      data->data().AddAttr(buzz::QN_FROM, remote_client_.jid().Str());
+      if (!response) {
+        local_sm_.OnIncomingMessage(&data->data());
+      } else {
+        local_sm_.OnIncomingResponse(NULL, &data->data());
+      }
+    } else if (message->message_id == MSG_LSIGNAL) {
+      data->data().AddAttr(buzz::QN_FROM, local_client_.jid().Str());
+      if (!response) {
+        remote_sm_.OnIncomingMessage(&data->data());
+      } else {
+        remote_sm_.OnIncomingResponse(NULL, &data->data());
+      }
+    }
+    delete data;
+  }
+
+  // Accept the tunnel when it arrives and wire up the stream.
+  void OnIncomingTunnel(cricket::TunnelSessionClient* client,
+                        buzz::Jid jid, std::string description,
+                        cricket::Session* session) {
+    remote_tunnel_.reset(remote_client_.AcceptTunnel(session));
+    remote_tunnel_->SignalEvent.connect(this,
+        &TunnelSessionClientTest::OnStreamEvent);
+  }
+
+  // Send from send_stream_ as long as we're not flow-controlled.
+  // Read bytes out into recv_stream_ as they arrive.
+  // End the test when we are notified that the local side has closed the
+  // tunnel. All data has been read out at this point.
+  void OnStreamEvent(talk_base::StreamInterface* stream, int events,
+                     int error) {
+    if (events & talk_base::SE_READ) {
+      if (stream == remote_tunnel_.get()) {
+        ReadData();
+      }
+    }
+    if (events & talk_base::SE_WRITE) {
+      if (stream == local_tunnel_.get()) {
+        bool done = false;
+        WriteData(&done);
+        if (done) {
+          local_tunnel_->Close();
+        }
+      }
+    }
+    if (events & talk_base::SE_CLOSE) {
+      if (stream == remote_tunnel_.get()) {
+        remote_tunnel_->Close();
+        done_ = true;
+      }
+    }
+  }
+
+  // Spool from the tunnel into recv_stream.
+  // Flow() doesn't work here because it won't write if the read blocks.
+  void ReadData() {
+    char block[kBlockSize];
+    size_t read, position;
+    talk_base::StreamResult res;
+    while ((res = remote_tunnel_->Read(block, sizeof(block), &read, NULL)) ==
+        talk_base::SR_SUCCESS) {
+      recv_stream_.Write(block, read, NULL, NULL);
+    }
+    ASSERT(res != talk_base::SR_EOS);
+    recv_stream_.GetPosition(&position);
+    LOG(LS_VERBOSE) << "Recv position: " << position;
+  }
+  // Spool from send_stream into the tunnel. Back up if we get flow controlled.
+  void WriteData(bool* done) {
+    char block[kBlockSize];
+    size_t leftover = 0, position;
+    talk_base::StreamResult res = talk_base::Flow(&send_stream_,
+        block, sizeof(block), local_tunnel_.get(), &leftover);
+    if (res == talk_base::SR_BLOCK) {
+      send_stream_.GetPosition(&position);
+      send_stream_.SetPosition(position - leftover);
+      LOG(LS_VERBOSE) << "Send position: " << position - leftover;
+      *done = false;
+    } else if (res == talk_base::SR_SUCCESS) {
+      *done = true;
+    } else {
+      ASSERT(false);  // shouldn't happen
+    }
+  }
+
+ private:
+  cricket::FakePortAllocator local_pa_;
+  cricket::FakePortAllocator remote_pa_;
+  cricket::SessionManager local_sm_;
+  cricket::SessionManager remote_sm_;
+  cricket::TunnelSessionClient local_client_;
+  cricket::TunnelSessionClient remote_client_;
+  talk_base::scoped_ptr<talk_base::StreamInterface> local_tunnel_;
+  talk_base::scoped_ptr<talk_base::StreamInterface> remote_tunnel_;
+  talk_base::MemoryStream send_stream_;
+  talk_base::MemoryStream recv_stream_;
+  bool done_;
+};
+
+// Test the normal case of sending data from one side to the other.
+TEST_F(TunnelSessionClientTest, TestTransfer) {
+  TestTransfer(1000000);
+}
diff --git a/talk/xmpp/chatroommodule.h b/talk/xmpp/chatroommodule.h
new file mode 100644
index 0000000..fddbecd
--- /dev/null
+++ b/talk/xmpp/chatroommodule.h
@@ -0,0 +1,258 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#ifndef _multiuserchatmodule_h_
+#define _multiuserchatmodule_h_
+
+#include "talk/xmpp/module.h"
+#include "talk/xmpp/rostermodule.h"
+
+namespace buzz {
+
+// forward declarations
+class XmppChatroomModule;
+class XmppChatroomHandler;
+class XmppChatroomMember;
+class XmppChatroomMemberEnumerator;
+
+enum XmppChatroomState {
+  XMPP_CHATROOM_STATE_NOT_IN_ROOM      = 0,
+  XMPP_CHATROOM_STATE_REQUESTED_ENTER  = 1,
+  XMPP_CHATROOM_STATE_IN_ROOM          = 2,
+  XMPP_CHATROOM_STATE_REQUESTED_EXIT   = 3,
+};
+
+//! Module that encapsulates a chatroom. 
+class XmppChatroomModule : public XmppModule {
+public:
+
+  //! Creates a new XmppChatroomModule
+  static XmppChatroomModule* Create();
+  virtual ~XmppChatroomModule() {}
+
+  //! Sets the chatroom handler (callbacks) for the chatroom
+  virtual XmppReturnStatus set_chatroom_handler(XmppChatroomHandler* handler) = 0;
+
+  //! Gets the chatroom handler for the module
+  virtual XmppChatroomHandler* chatroom_handler() = 0;
+
+  //! Sets the jid of the chatroom.
+  //! Has to be set before entering the chatroom and can't be changed
+  //! while in the chatroom
+  virtual XmppReturnStatus set_chatroom_jid(const Jid& chatroom_jid) = 0;
+
+  //! The jid for the chatroom
+  virtual const Jid& chatroom_jid() const = 0;
+
+  //! Sets the nickname of the member
+  //! Has to be set before entering the chatroom and can't be changed
+  //! while in the chatroom
+  virtual XmppReturnStatus set_nickname(const std::string& nickname) = 0;
+
+  //! The nickname of the member in the chatroom
+  virtual const std::string& nickname() const = 0;
+
+  //! Returns the jid of the member (this is the chatroom_jid plus the
+  //! nickname as the resource name)
+  virtual const Jid member_jid() const = 0;
+
+  //! Requests that the user enter a chatroom
+  //! The EnterChatroom callback will be called when the request is complete.
+  //! Password should be empty for a room that doesn't require a password
+  //! If the room doesn't exist, the server will create an "Instant Room" if the 
+  //! server policy supports this action.
+  //! There will be different methods for creating/configuring a "Reserved Room"
+  //! Async callback for this method is ChatroomEnteredStatus
+  virtual XmppReturnStatus RequestEnterChatroom(const std::string& password) = 0;
+
+  //! Requests that the user exit a chatroom
+  //! Async callback for this method is ChatroomExitedStatus
+  virtual XmppReturnStatus RequestExitChatroom() = 0;
+
+  //! Requests a status change
+  //! status is the standard XMPP status code
+  //! extended_status is the extended status when status is XMPP_PRESENCE_XA
+  virtual XmppReturnStatus RequestStatusChange(XmppPresenceShow status, 
+                                               const std::string& extended_status) = 0;
+
+  //! Returns the number of members in the room
+  virtual size_t GetChatroomMemberCount() = 0;
+
+  //! Gets an enumerator for the members in the chatroom
+  //! The caller must delete the enumerator when the caller is finished with it. 
+  //! The caller must also ensure that the lifetime of the enumerator is 
+  //! scoped by the XmppChatRoomModule that created it.
+  virtual XmppReturnStatus CreateMemberEnumerator(XmppChatroomMemberEnumerator** enumerator) = 0;
+
+  //! Gets the subject of the chatroom
+  virtual const std::string& subject() = 0;
+
+  //! Returns the current state of the user with respect to the chatroom
+  virtual XmppChatroomState state() = 0;
+
+  virtual XmppReturnStatus SendMessage(const XmlElement& message) = 0;
+};
+
+//! Class for enumerating participatns
+class XmppChatroomMemberEnumerator {
+public:
+  virtual ~XmppChatroomMemberEnumerator() { }
+  //! Returns the member at the current position
+  //! Returns null if the enumerator is before the beginning
+  //! or after the end of the collection
+  virtual XmppChatroomMember* current() = 0;
+
+  //! Returns whether the enumerator is valid
+  //! This returns true if the collection has changed
+  //! since the enumerator was created
+  virtual bool IsValid() = 0;
+
+  //! Returns whether the enumerator is before the beginning
+  //! This is the initial state of the enumerator
+  virtual bool IsBeforeBeginning() = 0;
+
+  //! Returns whether the enumerator is after the end
+  virtual bool IsAfterEnd() = 0;
+
+  //! Advances the enumerator to the next position
+  //! Returns false is the enumerator is advanced
+  //! off the end of the collection
+  virtual bool Next() = 0;
+
+  //! Advances the enumerator to the previous position
+  //! Returns false is the enumerator is advanced
+  //! off the end of the collection
+  virtual bool Prev() = 0;
+};
+
+
+//! Represents a single member in a chatroom
+class XmppChatroomMember {
+public:
+  virtual ~XmppChatroomMember() { }
+    
+  //! The jid for the member in the chatroom
+  virtual const Jid member_jid() const = 0;
+
+  //! The full jid for the member 
+  //! This is only available in non-anonymous rooms.
+  //! If the room is anonymous, this returns JID_EMPTY
+  virtual const Jid full_jid() const = 0;
+
+   //! Returns the backing presence for this member
+  virtual const XmppPresence* presence() const = 0;
+
+  //! The nickname for this member
+  virtual const std::string name() const = 0;
+};
+
+//! Status codes for ChatroomEnteredStatus callback
+enum XmppChatroomEnteredStatus
+{
+  //! User successfully entered the room
+  XMPP_CHATROOM_ENTERED_SUCCESS                    = 0,
+  //! The nickname confliced with somebody already in the room
+  XMPP_CHATROOM_ENTERED_FAILURE_NICKNAME_CONFLICT  = 1,
+  //! A password is required to enter the room
+  XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_REQUIRED  = 2,
+  //! The specified password was incorrect
+  XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_INCORRECT = 3,
+  //! The user is not a member of a member-only room
+  XMPP_CHATROOM_ENTERED_FAILURE_NOT_A_MEMBER       = 4,
+  //! The user cannot enter because the user has been banned
+  XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BANNED      = 5,
+  //! The room has the maximum number of users already
+  XMPP_CHATROOM_ENTERED_FAILURE_MAX_USERS          = 6,
+  //! The room has been locked by an administrator
+  XMPP_CHATROOM_ENTERED_FAILURE_ROOM_LOCKED        = 7,
+  //! Some other reason
+  XMPP_CHATROOM_ENTERED_FAILURE_UNSPECIFIED        = 2000,
+};
+
+//! Status codes for ChatroomExitedStatus callback
+enum XmppChatroomExitedStatus
+{
+  //! The user requested to exit and did so
+  XMPP_CHATROOM_EXITED_REQUESTED                   = 0,
+  //! The user was banned from the room
+  XMPP_CHATROOM_EXITED_BANNED                      = 1,
+  //! The user has been kicked out of the room
+  XMPP_CHATROOM_EXITED_KICKED                      = 2,
+  //! The user has been removed from the room because the
+  //! user is no longer a member of a member-only room
+  //! or the room has changed to membership-only
+  XMPP_CHATROOM_EXITED_NOT_A_MEMBER                = 3,
+  //! The system is shutting down
+  XMPP_CHATROOM_EXITED_SYSTEM_SHUTDOWN             = 4,
+  //! For some other reason
+  XMPP_CHATROOM_EXITED_UNSPECIFIED                 = 5,             
+};
+
+//! The XmppChatroomHandler is the interface for callbacks from the
+//! the chatroom
+class XmppChatroomHandler {
+public:
+  //! Indicates the response to RequestEnterChatroom method
+  //! XMPP_CHATROOM_SUCCESS represents success.
+  //! Other status codes are for errors
+  virtual void ChatroomEnteredStatus(XmppChatroomModule* room,
+                                     XmppChatroomEnteredStatus status) = 0;
+
+
+  //! Indicates that the user has exited the chatroom, either due to 
+  //! a call to RequestExitChatroom or for some other reason.
+  //! status indicates the reason the user exited
+  virtual void ChatroomExitedStatus(XmppChatroomModule* room,
+                                    XmppChatroomExitedStatus status) = 0;
+
+  //! Indicates a member entered the room.
+  virtual void MemberEntered(XmppChatroomModule* room,
+                                  const XmppChatroomMember* entered_member) = 0;
+
+  //! Indicates that a member exited the room.
+  virtual void MemberExited(XmppChatroomModule* room,
+                              const XmppChatroomMember* exited_member) = 0;
+
+  //! Indicates that the data for the member has changed
+  //! (such as the nickname or presence)
+  virtual void MemberChanged(XmppChatroomModule* room,
+                              size_t index) = 0;
+
+  //! Indicates a new message has been received
+  //! message is the message - 
+  // $TODO - message should be changed
+  //! to a strongly-typed message class that contains info
+  //! such as the sender, message bodies, etc.,
+  virtual void MessageReceived(XmppChatroomModule* room,
+                               const XmlElement& message) = 0;
+};
+
+
+}
+
+#endif
+
diff --git a/talk/xmpp/chatroommodule_unittest.cc b/talk/xmpp/chatroommodule_unittest.cc
new file mode 100644
index 0000000..b857551
--- /dev/null
+++ b/talk/xmpp/chatroommodule_unittest.cc
@@ -0,0 +1,295 @@
+/*
+ * libjingle
+ * Copyright 2004, 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.
+ */
+
+#include <string>
+#include <sstream>
+#include <iostream>
+#include "common/common.h"
+#include "buzz/xmppengine.h"
+#include "buzz/xmlelement.h"
+#include "buzz/chatroommodule.h"
+#include "buzz/constants.h"
+#include "engine/util_unittest.h"
+#include "test/unittest.h"
+#include "test/unittest-inl.h"
+
+#define TEST_OK(x) TEST_EQ((x),XMPP_RETURN_OK)
+#define TEST_BADARGUMENT(x) TEST_EQ((x),XMPP_RETURN_BADARGUMENT)
+
+namespace buzz {
+
+class MultiUserChatModuleTest;
+
+static void
+WriteEnteredStatus(std::ostream& os, XmppChatroomEnteredStatus status) {
+  switch(status) {
+    case XMPP_CHATROOM_ENTERED_SUCCESS:
+      os<<"success";
+      break;
+    case XMPP_CHATROOM_ENTERED_FAILURE_NICKNAME_CONFLICT:
+      os<<"failure(nickname conflict)";
+      break;
+    case XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_REQUIRED:
+      os<<"failure(password required)";
+      break;
+    case XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_INCORRECT:
+      os<<"failure(password incorrect)";
+      break;
+    case XMPP_CHATROOM_ENTERED_FAILURE_NOT_A_MEMBER:
+      os<<"failure(not a member)";
+      break;
+    case XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BANNED:
+      os<<"failure(member banned)";
+      break;
+    case XMPP_CHATROOM_ENTERED_FAILURE_MAX_USERS:
+      os<<"failure(max users)";
+      break;
+    case XMPP_CHATROOM_ENTERED_FAILURE_ROOM_LOCKED:
+      os<<"failure(room locked)";
+      break;
+    case XMPP_CHATROOM_ENTERED_FAILURE_UNSPECIFIED:
+      os<<"failure(unspecified)";
+      break;
+    default:
+      os<<"unknown";
+      break;
+  } 
+}
+
+static void
+WriteExitedStatus(std::ostream& os, XmppChatroomExitedStatus status) {
+  switch (status) {
+    case XMPP_CHATROOM_EXITED_REQUESTED:
+      os<<"requested";
+      break;
+    case XMPP_CHATROOM_EXITED_BANNED:
+      os<<"banned";
+      break;
+    case XMPP_CHATROOM_EXITED_KICKED:
+      os<<"kicked";
+      break;
+    case XMPP_CHATROOM_EXITED_NOT_A_MEMBER:
+      os<<"not member";
+      break;
+    case XMPP_CHATROOM_EXITED_SYSTEM_SHUTDOWN:
+      os<<"system shutdown";
+      break;
+    case XMPP_CHATROOM_EXITED_UNSPECIFIED:
+      os<<"unspecified";
+      break;
+    default:
+      os<<"unknown";
+      break;
+  }
+}
+
+//! This session handler saves all calls to a string.  These are events and
+//! data delivered form the engine to application code.
+class XmppTestChatroomHandler : public XmppChatroomHandler {
+public:
+  XmppTestChatroomHandler() {}
+  virtual ~XmppTestChatroomHandler() {}
+
+  void ChatroomEnteredStatus(XmppChatroomModule* room,
+                             XmppChatroomEnteredStatus status) {
+    UNUSED(room);
+    ss_ <<"[ChatroomEnteredStatus status: ";
+    WriteEnteredStatus(ss_, status);
+    ss_ <<"]";
+  }
+
+
+  void ChatroomExitedStatus(XmppChatroomModule* room,
+                            XmppChatroomExitedStatus status) {
+    UNUSED(room);
+    ss_ <<"[ChatroomExitedStatus status: ";
+    WriteExitedStatus(ss_, status);
+    ss_ <<"]";
+  }
+
+  void MemberEntered(XmppChatroomModule* room, 
+                          const XmppChatroomMember* entered_member) {
+    UNUSED(room);
+    ss_ << "[MemberEntered " << entered_member->member_jid().Str() << "]";
+  }
+
+  void MemberExited(XmppChatroomModule* room,
+                         const XmppChatroomMember* exited_member) {
+    UNUSED(room);
+    ss_ << "[MemberExited " << exited_member->member_jid().Str() << "]";
+  }
+
+  void MemberChanged(XmppChatroomModule* room, size_t index) {
+    UNUSED2(room, index);
+  }
+
+  virtual void MessageReceived(XmppChatroomModule* room, const XmlElement& message) {
+    UNUSED2(room, message);
+  }
+
+ 
+  std::string Str() {
+    return ss_.str();
+  }
+
+  std::string StrClear() {
+    std::string result = ss_.str();
+    ss_.str("");
+    return result;
+  }
+
+private:
+  std::stringstream ss_;
+};
+
+//! This is the class that holds all of the unit test code for the
+//! roster module
+class XmppChatroomModuleTest : public UnitTest {
+public:
+  XmppChatroomModuleTest() {}
+
+  void TestEnterExitChatroom() {
+    std::stringstream dump;
+
+    // Configure the engine
+    scoped_ptr<XmppEngine> engine(XmppEngine::Create());
+    XmppTestHandler handler(engine.get());
+
+    // Configure the module and handler
+    scoped_ptr<XmppChatroomModule> chatroom(XmppChatroomModule::Create());
+
+    // Configure the module handler
+    chatroom->RegisterEngine(engine.get());
+
+    // Set up callbacks
+    engine->SetOutputHandler(&handler);
+    engine->AddStanzaHandler(&handler);
+    engine->SetSessionHandler(&handler);
+
+    // Set up minimal login info
+    engine->SetUser(Jid("david@my-server"));
+    engine->SetPassword("david");
+
+    // Do the whole login handshake
+    RunLogin(this, engine.get(), &handler);
+    TEST_EQ("", handler.OutputActivity());
+
+    // Get the chatroom and set the handler
+    XmppTestChatroomHandler chatroom_handler;
+    chatroom->set_chatroom_handler(static_cast<XmppChatroomHandler*>(&chatroom_handler));
+
+    // try to enter the chatroom
+    TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_NOT_IN_ROOM);
+    chatroom->set_nickname("thirdwitch");
+    chatroom->set_chatroom_jid(Jid("darkcave@my-server"));
+    chatroom->RequestEnterChatroom("");
+    TEST_EQ(chatroom_handler.StrClear(), "");
+    TEST_EQ(handler.OutputActivity(),
+      "<presence to=\"darkcave@my-server/thirdwitch\">"
+        "<muc:x xmlns:muc=\"http://jabber.org/protocol/muc\"/>"
+      "</presence>");
+    TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_REQUESTED_ENTER);
+
+    // simulate the server and test the client
+    std::string input;
+    input = "<presence from=\"darkcave@my-server/firstwitch\" to=\"david@my-server\">"
+             "<x xmlns=\"http://jabber.org/protocol/muc#user\">"
+              "<item affiliation=\"owner\" role=\"participant\"/>"
+             "</x>"
+            "</presence>";
+    TEST_OK(engine->HandleInput(input.c_str(), input.length()));
+    TEST_EQ(chatroom_handler.StrClear(), "");
+    TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_REQUESTED_ENTER);
+
+    input = "<presence from=\"darkcave@my-server/secondwitch\" to=\"david@my-server\">"
+             "<x xmlns=\"http://jabber.org/protocol/muc#user\">"
+              "<item affiliation=\"member\" role=\"participant\"/>"
+             "</x>"
+            "</presence>";
+    TEST_OK(engine->HandleInput(input.c_str(), input.length()));
+    TEST_EQ(chatroom_handler.StrClear(), "");
+    TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_REQUESTED_ENTER);
+
+    input = "<presence from=\"darkcave@my-server/thirdwitch\" to=\"david@my-server\">"
+             "<x xmlns=\"http://jabber.org/protocol/muc#user\">"
+              "<item affiliation=\"member\" role=\"participant\"/>"
+             "</x>"
+            "</presence>";
+    TEST_OK(engine->HandleInput(input.c_str(), input.length()));
+    TEST_EQ(chatroom_handler.StrClear(),
+      "[ChatroomEnteredStatus status: success]");
+    TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_IN_ROOM);
+
+    // simulate somebody else entering the room after we entered
+    input = "<presence from=\"darkcave@my-server/fourthwitch\" to=\"david@my-server\">"
+             "<x xmlns=\"http://jabber.org/protocol/muc#user\">"
+              "<item affiliation=\"member\" role=\"participant\"/>"
+             "</x>"
+            "</presence>";
+    TEST_OK(engine->HandleInput(input.c_str(), input.length()));
+    TEST_EQ(chatroom_handler.StrClear(), "[MemberEntered darkcave@my-server/fourthwitch]");
+    TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_IN_ROOM);
+
+    // simulate somebody else leaving the room after we entered
+    input = "<presence from=\"darkcave@my-server/secondwitch\" to=\"david@my-server\" type=\"unavailable\">"
+             "<x xmlns=\"http://jabber.org/protocol/muc#user\">"
+              "<item affiliation=\"member\" role=\"participant\"/>"
+             "</x>"
+            "</presence>";
+    TEST_OK(engine->HandleInput(input.c_str(), input.length()));
+    TEST_EQ(chatroom_handler.StrClear(), "[MemberExited darkcave@my-server/secondwitch]");
+    TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_IN_ROOM);
+
+    // try to leave the room
+    chatroom->RequestExitChatroom();
+    TEST_EQ(chatroom_handler.StrClear(), "");
+    TEST_EQ(handler.OutputActivity(),
+      "<presence to=\"darkcave@my-server/thirdwitch\" type=\"unavailable\"/>");
+    TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_REQUESTED_EXIT);
+
+    // simulate the server and test the client
+    input = "<presence from=\"darkcave@my-server/thirdwitch\" to=\"david@my-server\" type=\"unavailable\">"
+             "<x xmlns=\"http://jabber.org/protocol/muc#user\">"
+              "<item affiliation=\"member\" role=\"participant\"/>"
+             "</x>"
+            "</presence>";
+    TEST_OK(engine->HandleInput(input.c_str(), input.length()));
+    TEST_EQ(chatroom_handler.StrClear(),
+      "[ChatroomExitedStatus status: requested]");
+    TEST_EQ(chatroom->state(), XMPP_CHATROOM_STATE_NOT_IN_ROOM);
+  }
+
+};
+
+// A global function that creates the test suite for this set of tests.
+TestBase* ChatroomModuleTest_Create() {
+  TestSuite* suite = new TestSuite("ChatroomModuleTest");
+  ADD_TEST(suite, XmppChatroomModuleTest, TestEnterExitChatroom);
+  return suite;
+}
+
+}
diff --git a/talk/xmpp/chatroommoduleimpl.cc b/talk/xmpp/chatroommoduleimpl.cc
new file mode 100644
index 0000000..0fbbe7e
--- /dev/null
+++ b/talk/xmpp/chatroommoduleimpl.cc
@@ -0,0 +1,676 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#include <vector>
+#include <string>
+#include <map>
+#include <algorithm>
+#include <sstream>
+#include <iostream>
+#include "talk/base/common.h"
+#include "talk/xmpp/constants.h"
+#include "talk/xmpp/moduleimpl.h"
+#include "talk/xmpp/chatroommodule.h"
+
+namespace buzz {
+
+// forward declarations
+class XmppChatroomImpl;
+class XmppChatroomMemberImpl;
+
+//! Module that encapsulates multiple chatrooms.
+//! Each chatroom is represented by an XmppChatroomImpl instance
+class XmppChatroomModuleImpl : public XmppChatroomModule,
+  public XmppModuleImpl, public XmppIqHandler {
+public:
+  IMPLEMENT_XMPPMODULE
+
+   // Creates a chatroom with specified Jid
+  XmppChatroomModuleImpl();
+  ~XmppChatroomModuleImpl();
+
+  // XmppChatroomModule
+  virtual XmppReturnStatus set_chatroom_handler(XmppChatroomHandler* handler);
+  virtual XmppChatroomHandler* chatroom_handler();
+  virtual XmppReturnStatus set_chatroom_jid(const Jid& chatroom_jid);
+  virtual const Jid& chatroom_jid() const;
+  virtual XmppReturnStatus set_nickname(const std::string& nickname);
+  virtual const std::string& nickname() const;
+  virtual const Jid member_jid() const;
+  virtual XmppReturnStatus RequestEnterChatroom(const std::string& password);
+  virtual XmppReturnStatus RequestExitChatroom();
+  virtual XmppReturnStatus RequestStatusChange(XmppPresenceShow status, 
+                                       const std::string& extended_status);
+  virtual size_t GetChatroomMemberCount();
+  virtual XmppReturnStatus CreateMemberEnumerator(XmppChatroomMemberEnumerator** enumerator);
+  virtual const std::string& subject();
+  virtual XmppChatroomState state() { return chatroom_state_; }
+  virtual XmppReturnStatus SendMessage(const XmlElement& message);
+
+  // XmppModule
+  virtual void IqResponse(XmppIqCookie cookie, const XmlElement * pelStanza) {UNUSED2(cookie, pelStanza);}
+  virtual bool HandleStanza(const XmlElement *);
+
+private:
+  friend class XmppChatroomMemberEnumeratorImpl;
+
+  XmppReturnStatus ServerChangeMyPresence(const XmlElement& presence);
+  XmppReturnStatus ClientChangeMyPresence(XmppChatroomState new_state);
+  XmppReturnStatus ChangePresence(XmppChatroomState new_state, const XmlElement* presence, bool isServer);
+  XmppReturnStatus ServerChangedOtherPresence(const XmlElement& presence_element);
+  XmppChatroomEnteredStatus GetEnterFailureFromXml(const XmlElement* presence);
+  XmppChatroomExitedStatus GetExitFailureFromXml(const XmlElement* presence);
+  
+  bool CheckEnterChatroomStateOk();
+
+  void FireEnteredStatus(XmppChatroomEnteredStatus status);
+  void FireExitStatus(XmppChatroomExitedStatus status);
+  void FireMessageReceived(const XmlElement& message);
+  void FireMemberEntered(const XmppChatroomMember* entered_member);
+  void FireMemberExited(const XmppChatroomMember* exited_member);
+
+
+  typedef std::map<Jid, XmppChatroomMemberImpl*> JidMemberMap;
+  
+  XmppChatroomHandler*              chatroom_handler_;
+  Jid                               chatroom_jid_;
+  std::string                       nickname_;
+  XmppChatroomState                 chatroom_state_;
+  JidMemberMap                      chatroom_jid_members_;
+  int                               chatroom_jid_members_version_;
+};
+
+
+class XmppChatroomMemberImpl : public XmppChatroomMember {
+public:
+  ~XmppChatroomMemberImpl() {}
+  XmppReturnStatus SetPresence(const XmppPresence* presence);
+
+  // XmppChatroomMember
+  const Jid member_jid() const;
+  const Jid full_jid() const;
+  const std::string name() const;
+  const XmppPresence* presence() const;
+
+private:
+  talk_base::scoped_ptr<XmppPresence>  presence_;
+};
+
+class XmppChatroomMemberEnumeratorImpl : 
+        public XmppChatroomMemberEnumerator  {
+public:
+  XmppChatroomMemberEnumeratorImpl(XmppChatroomModuleImpl::JidMemberMap* chatroom_jid_members,
+                                        int* map_version);
+
+  // XmppChatroomMemberEnumerator
+  virtual XmppChatroomMember* current();
+  virtual bool Next();
+  virtual bool Prev();
+  virtual bool IsValid();
+  virtual bool IsBeforeBeginning();
+  virtual bool IsAfterEnd();
+
+private:
+  XmppChatroomModuleImpl::JidMemberMap*           map_;
+  int                                             map_version_created_;
+  int*                                            map_version_;
+  XmppChatroomModuleImpl::JidMemberMap::iterator  iterator_;
+  bool                                            before_beginning_;
+};
+
+
+// XmppChatroomModuleImpl ------------------------------------------------
+XmppChatroomModule *
+XmppChatroomModule::Create() {
+  return new XmppChatroomModuleImpl();
+}
+
+XmppChatroomModuleImpl::XmppChatroomModuleImpl() :
+  chatroom_handler_(NULL), 
+  chatroom_jid_(STR_EMPTY),
+  chatroom_state_(XMPP_CHATROOM_STATE_NOT_IN_ROOM),
+  chatroom_jid_members_version_(0) {
+}
+
+XmppChatroomModuleImpl::~XmppChatroomModuleImpl() {
+  JidMemberMap::iterator iterator = chatroom_jid_members_.begin();
+  while (iterator != chatroom_jid_members_.end()) {
+    delete iterator->second;
+    iterator++;
+  }
+}
+
+
+bool 
+XmppChatroomModuleImpl::HandleStanza(const XmlElement* stanza) {
+  ASSERT(engine() != NULL);
+
+  // we handle stanzas that are for one of our chatrooms
+  Jid from_jid = Jid(stanza->Attr(QN_FROM));
+  // see if it's one of our chatrooms
+  if (chatroom_jid_ != from_jid.BareJid()) {
+    return false; // not one of our chatrooms
+  } else {
+    // handle presence stanza
+    if (stanza->Name() == QN_PRESENCE) {
+      if (from_jid == member_jid()) {
+        ServerChangeMyPresence(*stanza);
+      } else {
+        ServerChangedOtherPresence(*stanza);
+      }
+    } else if (stanza->Name() == QN_MESSAGE) {
+      FireMessageReceived(*stanza);
+    }
+    return true;
+  }
+}
+
+
+XmppReturnStatus
+XmppChatroomModuleImpl::set_chatroom_handler(XmppChatroomHandler* handler) {
+  // Calling with NULL removes the handler.
+  chatroom_handler_ = handler;
+  return XMPP_RETURN_OK;
+}
+
+
+XmppChatroomHandler* 
+XmppChatroomModuleImpl::chatroom_handler() {
+  return chatroom_handler_;
+}
+
+XmppReturnStatus 
+XmppChatroomModuleImpl::set_chatroom_jid(const Jid& chatroom_jid) {
+  if (chatroom_state_ != XMPP_CHATROOM_STATE_NOT_IN_ROOM) {
+    return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call,  diff error code?
+  }
+  if (chatroom_jid != chatroom_jid.BareJid()) {
+    // chatroom_jid must be a bare jid
+    return XMPP_RETURN_BADARGUMENT;
+  }
+  
+  chatroom_jid_ = chatroom_jid;
+  return XMPP_RETURN_OK;
+}
+
+const Jid& 
+XmppChatroomModuleImpl::chatroom_jid() const {
+  return chatroom_jid_;
+}
+
+ XmppReturnStatus 
+ XmppChatroomModuleImpl::set_nickname(const std::string& nickname) {
+  if (chatroom_state_ != XMPP_CHATROOM_STATE_NOT_IN_ROOM) {
+    return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call,  diff error code?
+  }
+  nickname_ = nickname;
+  return XMPP_RETURN_OK;
+ }
+
+ const std::string& 
+ XmppChatroomModuleImpl::nickname() const {
+  return nickname_;
+ }
+
+const Jid
+XmppChatroomModuleImpl::member_jid() const {
+  return Jid(chatroom_jid_.node(), chatroom_jid_.domain(), nickname_);
+}
+
+
+bool 
+XmppChatroomModuleImpl::CheckEnterChatroomStateOk() {
+  if (chatroom_jid_.IsValid() == false) {
+    ASSERT(0);
+    return false;
+  }
+  if (nickname_ == STR_EMPTY) {
+    ASSERT(0);
+    return false;
+  }
+  return true;
+}
+
+XmppReturnStatus 
+XmppChatroomModuleImpl::RequestEnterChatroom(const std::string& password) {
+  UNUSED(password);
+  if (!engine())
+    return XMPP_RETURN_BADSTATE;
+
+  if (chatroom_state_ != XMPP_CHATROOM_STATE_NOT_IN_ROOM)
+    return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call,  diff error code?
+
+  if (CheckEnterChatroomStateOk() == false) {
+    return XMPP_RETURN_BADSTATE;
+  }
+
+  // entering a chatroom is a presence request to the server
+  XmlElement element(QN_PRESENCE);
+  element.AddAttr(QN_TO, member_jid().Str());
+  element.AddElement(new XmlElement(QN_MUC_X));
+  XmppReturnStatus status = engine()->SendStanza(&element);
+  if (status == XMPP_RETURN_OK) {
+    return ClientChangeMyPresence(XMPP_CHATROOM_STATE_REQUESTED_ENTER);
+  }
+  return status;
+}
+
+
+XmppReturnStatus 
+XmppChatroomModuleImpl::RequestExitChatroom() {
+  if (!engine())
+    return XMPP_RETURN_BADSTATE;
+
+  // currently, can't leave a room unless you've entered
+  // no way to cancel a pending enter call - is that bad?
+  if (chatroom_state_ != XMPP_CHATROOM_STATE_IN_ROOM)
+    return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call,  diff error code?
+
+  // exiting a chatroom is a presence request to the server
+  XmlElement element(QN_PRESENCE);
+  element.AddAttr(QN_TO, member_jid().Str());
+  element.AddAttr(QN_TYPE, "unavailable");
+  XmppReturnStatus status = engine()->SendStanza(&element);
+  if (status == XMPP_RETURN_OK) {
+    return ClientChangeMyPresence(XMPP_CHATROOM_STATE_REQUESTED_EXIT);
+  }
+  return status;
+}
+
+XmppReturnStatus 
+XmppChatroomModuleImpl::RequestStatusChange(XmppPresenceShow status, 
+                                     const std::string& extended_status) {
+  UNUSED2(status, extended_status);
+  return XMPP_RETURN_BADSTATE; //NYI
+}
+
+
+
+size_t 
+XmppChatroomModuleImpl::GetChatroomMemberCount() {
+  return chatroom_jid_members_.size();
+}
+
+XmppReturnStatus 
+XmppChatroomModuleImpl::CreateMemberEnumerator(XmppChatroomMemberEnumerator** enumerator) {
+  *enumerator = new XmppChatroomMemberEnumeratorImpl(&chatroom_jid_members_, &chatroom_jid_members_version_);
+  return XMPP_RETURN_OK;
+}
+
+const std::string& 
+XmppChatroomModuleImpl::subject() {
+  return STR_EMPTY; //NYI
+}
+
+XmppReturnStatus 
+XmppChatroomModuleImpl::SendMessage(const XmlElement& message) {
+  XmppReturnStatus xmpp_status = XMPP_RETURN_OK;
+  
+  // can only send a message if we're in the room
+  if (chatroom_state_ != XMPP_CHATROOM_STATE_IN_ROOM) {
+    return XMPP_RETURN_BADSTATE; // $TODO - this isn't a bad state, it's a bad call,  diff error code?
+  }
+
+  if (message.Name() != QN_MESSAGE) {
+    IFR(XMPP_RETURN_BADARGUMENT);
+  }
+  
+  const std::string& type = message.Attr(QN_TYPE);
+  if (type != "groupchat") {
+    IFR(XMPP_RETURN_BADARGUMENT);
+  }
+
+  if (message.HasAttr(QN_FROM)) {
+    IFR(XMPP_RETURN_BADARGUMENT);
+  }
+
+  if (message.Attr(QN_TO) != chatroom_jid_.Str()) {
+    IFR(XMPP_RETURN_BADARGUMENT);
+  }
+
+  IFR(engine()->SendStanza(&message));
+
+  return xmpp_status;
+}
+
+enum TransitionType {
+  TRANSITION_TYPE_NONE                 = 0, 
+  TRANSITION_TYPE_ENTER_SUCCESS        = 1,
+  TRANSITION_TYPE_ENTER_FAILURE        = 2,
+  TRANSITION_TYPE_EXIT_VOLUNTARILY     = 3,
+  TRANSITION_TYPE_EXIT_INVOLUNTARILY   = 4,
+};
+
+struct StateTransitionDescription {
+  XmppChatroomState old_state;
+  XmppChatroomState new_state;
+  bool              is_valid_server_transition;
+  bool              is_valid_client_transition;
+  TransitionType    transition_type;
+};
+
+StateTransitionDescription Transitions[] = {
+  { XMPP_CHATROOM_STATE_NOT_IN_ROOM,     XMPP_CHATROOM_STATE_REQUESTED_ENTER, false, true,  TRANSITION_TYPE_NONE, },
+  { XMPP_CHATROOM_STATE_NOT_IN_ROOM,     XMPP_CHATROOM_STATE_IN_ROOM,         false, false, TRANSITION_TYPE_ENTER_SUCCESS, },
+  { XMPP_CHATROOM_STATE_NOT_IN_ROOM,     XMPP_CHATROOM_STATE_REQUESTED_EXIT,  false, false, TRANSITION_TYPE_NONE, },
+  { XMPP_CHATROOM_STATE_REQUESTED_ENTER, XMPP_CHATROOM_STATE_NOT_IN_ROOM,     true,  false, TRANSITION_TYPE_ENTER_FAILURE, },
+  { XMPP_CHATROOM_STATE_REQUESTED_ENTER, XMPP_CHATROOM_STATE_IN_ROOM,         true,  false, TRANSITION_TYPE_ENTER_SUCCESS, },
+  { XMPP_CHATROOM_STATE_REQUESTED_ENTER, XMPP_CHATROOM_STATE_REQUESTED_EXIT,  false, false, TRANSITION_TYPE_NONE, },
+  { XMPP_CHATROOM_STATE_IN_ROOM,         XMPP_CHATROOM_STATE_NOT_IN_ROOM,     true,  false, TRANSITION_TYPE_EXIT_INVOLUNTARILY,  },
+  { XMPP_CHATROOM_STATE_IN_ROOM,         XMPP_CHATROOM_STATE_REQUESTED_ENTER, false, false, TRANSITION_TYPE_NONE, },
+  { XMPP_CHATROOM_STATE_IN_ROOM,         XMPP_CHATROOM_STATE_REQUESTED_EXIT,  false, true,  TRANSITION_TYPE_NONE, },
+  { XMPP_CHATROOM_STATE_REQUESTED_EXIT,  XMPP_CHATROOM_STATE_NOT_IN_ROOM,     true,  false, TRANSITION_TYPE_EXIT_VOLUNTARILY, },
+  { XMPP_CHATROOM_STATE_REQUESTED_EXIT,  XMPP_CHATROOM_STATE_REQUESTED_ENTER, false, false, TRANSITION_TYPE_NONE, },
+  { XMPP_CHATROOM_STATE_REQUESTED_EXIT,  XMPP_CHATROOM_STATE_IN_ROOM,         false, false, TRANSITION_TYPE_NONE, },
+};
+
+
+
+void
+XmppChatroomModuleImpl::FireEnteredStatus(XmppChatroomEnteredStatus status) {
+  if (chatroom_handler_)
+    chatroom_handler_->ChatroomEnteredStatus(this, status);
+}
+
+void
+XmppChatroomModuleImpl::FireExitStatus(XmppChatroomExitedStatus status) {
+  if (chatroom_handler_)
+    chatroom_handler_->ChatroomExitedStatus(this, status);
+}
+
+void 
+XmppChatroomModuleImpl::FireMessageReceived(const XmlElement& message) {
+  if (chatroom_handler_)
+    chatroom_handler_->MessageReceived(this, message);
+}
+
+void 
+XmppChatroomModuleImpl::FireMemberEntered(const XmppChatroomMember* entered_member) {
+  // only fire if we're in the room
+  if (chatroom_state_ == XMPP_CHATROOM_STATE_IN_ROOM) {
+    if (chatroom_handler_)
+      chatroom_handler_->MemberEntered(this, entered_member);
+  }
+}
+
+void 
+XmppChatroomModuleImpl::FireMemberExited(const XmppChatroomMember* exited_member) {
+  // only fire if we're in the room
+  if (chatroom_state_ == XMPP_CHATROOM_STATE_IN_ROOM) {
+    if (chatroom_handler_)
+      chatroom_handler_->MemberExited(this, exited_member);
+  }
+}
+
+
+XmppReturnStatus 
+XmppChatroomModuleImpl::ServerChangedOtherPresence(const XmlElement& 
+                                                   presence_element) {
+  XmppReturnStatus xmpp_status = XMPP_RETURN_OK;
+  talk_base::scoped_ptr<XmppPresence> presence(XmppPresence::Create());
+  IFR(presence->set_raw_xml(&presence_element));
+
+  JidMemberMap::iterator pos = chatroom_jid_members_.find(presence->jid());
+
+  if (pos == chatroom_jid_members_.end()) {
+    if (presence->available() == XMPP_PRESENCE_AVAILABLE) { 
+      XmppChatroomMemberImpl* member = new XmppChatroomMemberImpl();
+      member->SetPresence(presence.get());
+      chatroom_jid_members_.insert(std::make_pair(member->member_jid(), member));
+      chatroom_jid_members_version_++;
+      FireMemberEntered(member);
+    }
+  } else {
+    XmppChatroomMemberImpl* member = pos->second;
+    if (presence->available() == XMPP_PRESENCE_AVAILABLE) {
+      member->SetPresence(presence.get());
+      chatroom_jid_members_version_++;
+      // $TODO - fire change
+    }
+    else if (presence->available() == XMPP_PRESENCE_UNAVAILABLE) {
+      chatroom_jid_members_.erase(pos);
+      chatroom_jid_members_version_++;
+      FireMemberExited(member);
+      delete member;
+    }
+  }
+
+  return xmpp_status;
+}
+
+XmppReturnStatus 
+XmppChatroomModuleImpl::ClientChangeMyPresence(XmppChatroomState new_state) {
+  return ChangePresence(new_state, NULL, false);
+}
+
+XmppReturnStatus 
+XmppChatroomModuleImpl::ServerChangeMyPresence(const XmlElement& presence) {
+   XmppChatroomState new_state;
+   
+   if (presence.HasAttr(QN_TYPE) == false) {
+      new_state = XMPP_CHATROOM_STATE_IN_ROOM;
+   } else {
+     new_state = XMPP_CHATROOM_STATE_NOT_IN_ROOM;
+   }
+  return ChangePresence(new_state, &presence, true);
+
+}
+
+XmppReturnStatus
+XmppChatroomModuleImpl::ChangePresence(XmppChatroomState new_state, 
+                                       const XmlElement* presence, 
+                                       bool isServer) {
+  UNUSED(presence);
+  
+  XmppChatroomState old_state = chatroom_state_;
+  
+  // do nothing if state hasn't changed
+  if (old_state == new_state)
+    return XMPP_RETURN_OK;
+
+  // find the right transition description
+  StateTransitionDescription* transition_desc = NULL;
+  for (int i=0; i < ARRAY_SIZE(Transitions); i++) {
+    if (Transitions[i].old_state == old_state &&
+        Transitions[i].new_state == new_state) {
+        transition_desc = &Transitions[i];
+        break;
+    }
+  }
+
+  if (transition_desc == NULL) {
+    ASSERT(0);
+    return XMPP_RETURN_BADSTATE;
+  }
+
+  // we assert for any invalid transition states, and we'll
+  if (isServer) {
+    // $TODO send original stanza back to server and log an error?
+    ASSERT(transition_desc->is_valid_server_transition);
+  } else {
+    if (transition_desc->is_valid_client_transition == false) {
+      ASSERT(0);
+      return XMPP_RETURN_BADARGUMENT;
+    }
+  }
+
+  // set the new state and then fire any notifications to the handler
+  chatroom_state_ = new_state;
+
+  switch (transition_desc->transition_type) {
+    case TRANSITION_TYPE_ENTER_SUCCESS:
+      FireEnteredStatus(XMPP_CHATROOM_ENTERED_SUCCESS);
+      break;
+    case TRANSITION_TYPE_ENTER_FAILURE:
+      FireEnteredStatus(GetEnterFailureFromXml(presence));
+      break;
+    case TRANSITION_TYPE_EXIT_INVOLUNTARILY:
+      FireExitStatus(GetExitFailureFromXml(presence));
+      break;
+    case TRANSITION_TYPE_EXIT_VOLUNTARILY:
+      FireExitStatus(XMPP_CHATROOM_EXITED_REQUESTED);
+      break;
+    case TRANSITION_TYPE_NONE:
+      break;
+  }
+
+  return XMPP_RETURN_OK;
+}
+
+XmppChatroomEnteredStatus 
+XmppChatroomModuleImpl::GetEnterFailureFromXml(const XmlElement* presence) {
+  XmppChatroomEnteredStatus status = XMPP_CHATROOM_ENTERED_FAILURE_UNSPECIFIED;
+  const XmlElement* error = presence->FirstNamed(QN_ERROR);
+  if (error != NULL && error->HasAttr(QN_CODE)) {
+    int code = atoi(error->Attr(QN_CODE).c_str());
+    switch (code) {
+      case 401: status = XMPP_CHATROOM_ENTERED_FAILURE_PASSWORD_REQUIRED; break;
+      case 403: status = XMPP_CHATROOM_ENTERED_FAILURE_MEMBER_BANNED; break;
+      case 405: status = XMPP_CHATROOM_ENTERED_FAILURE_MAX_USERS; break;
+      case 407: status = XMPP_CHATROOM_ENTERED_FAILURE_NOT_A_MEMBER; break;
+      case 409: status = XMPP_CHATROOM_ENTERED_FAILURE_NICKNAME_CONFLICT; break;
+    }
+  }
+  return status;
+}
+
+XmppChatroomExitedStatus 
+XmppChatroomModuleImpl::GetExitFailureFromXml(const XmlElement* presence) {
+  XmppChatroomExitedStatus status = XMPP_CHATROOM_EXITED_UNSPECIFIED;
+  const XmlElement* error = presence->FirstNamed(QN_ERROR);
+  if (error != NULL && error->HasAttr(QN_CODE)) {
+    int code = atoi(error->Attr(QN_CODE).c_str());
+    switch (code) {
+      case 307: status = XMPP_CHATROOM_EXITED_KICKED; break;
+      case 322: status = XMPP_CHATROOM_EXITED_NOT_A_MEMBER; break;
+      case 332: status = XMPP_CHATROOM_EXITED_SYSTEM_SHUTDOWN; break;
+    }
+  }
+  return status;
+}
+
+XmppReturnStatus
+XmppChatroomMemberImpl::SetPresence(const XmppPresence* presence) {
+  ASSERT(presence != NULL);
+  
+  // copy presence
+  presence_.reset(XmppPresence::Create());
+  presence_->set_raw_xml(presence->raw_xml());
+  return XMPP_RETURN_OK;
+}
+
+const Jid 
+XmppChatroomMemberImpl::member_jid() const {
+  return presence_->jid();
+}
+
+const Jid 
+XmppChatroomMemberImpl::full_jid() const {
+  return Jid("");
+}
+
+const std::string 
+XmppChatroomMemberImpl::name() const {
+  return member_jid().resource();
+}
+
+const XmppPresence* 
+XmppChatroomMemberImpl::presence() const {
+  return presence_.get();
+}
+
+
+// XmppChatroomMemberEnumeratorImpl --------------------------------------
+XmppChatroomMemberEnumeratorImpl::XmppChatroomMemberEnumeratorImpl(
+        XmppChatroomModuleImpl::JidMemberMap* map, int* map_version) {
+  map_ = map;
+  map_version_ = map_version;
+  map_version_created_ = *map_version_;
+  iterator_ = map->begin();
+  before_beginning_ = true;
+}
+
+XmppChatroomMember*
+XmppChatroomMemberEnumeratorImpl::current() {
+  if (IsValid() == false) {
+    return NULL;
+  } else if (IsBeforeBeginning() || IsAfterEnd()) {
+    return NULL;
+  } else {
+    return iterator_->second;
+  }
+}
+
+bool
+XmppChatroomMemberEnumeratorImpl::Prev() {
+  if (IsValid() == false) {
+    return false;
+  } else if (IsBeforeBeginning()) {
+    return false;
+  } else if (iterator_ == map_->begin()) {
+    before_beginning_ = true;
+    return false;
+  } else {
+    iterator_--;
+    return current() != NULL;
+  }
+}
+
+bool
+XmppChatroomMemberEnumeratorImpl::Next() {
+  if (IsValid() == false) {
+    return false;
+  } else if (IsBeforeBeginning()) {
+    before_beginning_ = false;
+    iterator_ = map_->begin();
+    return current() != NULL;
+  } else if (IsAfterEnd()) {
+    return false;
+  } else {
+    iterator_++;
+    return current() != NULL;
+  }
+}
+
+bool 
+XmppChatroomMemberEnumeratorImpl::IsValid() {
+  return map_version_created_ == *map_version_;
+}
+
+bool 
+XmppChatroomMemberEnumeratorImpl::IsBeforeBeginning() {
+  return before_beginning_;
+}
+
+bool 
+XmppChatroomMemberEnumeratorImpl::IsAfterEnd() {
+  return (iterator_ == map_->end());
+}
+
+
+
+} // namespace buzz
diff --git a/talk/xmpp/module.h b/talk/xmpp/module.h
new file mode 100644
index 0000000..75a190d
--- /dev/null
+++ b/talk/xmpp/module.h
@@ -0,0 +1,51 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#ifndef _module_h_
+#define _module_h_
+
+namespace buzz {
+
+class XmppEngine;
+enum XmppReturnStatus;
+
+//! This is the base class for extension modules.
+//! An engine is registered with the module and the module then hooks the
+//! appropriate parts of the engine to implement that set of features.  It is
+//! important to unregister modules before destructing the engine.
+class XmppModule {
+public:
+  virtual ~XmppModule() {}
+
+  //! Register the engine with the module.  Only one engine can be associated
+  //! with a module at a time.  This method will return an error if there is
+  //! already an engine registered.
+  virtual XmppReturnStatus RegisterEngine(XmppEngine* engine) = 0;
+};
+
+}
+#endif
diff --git a/talk/xmpp/moduleimpl.cc b/talk/xmpp/moduleimpl.cc
new file mode 100644
index 0000000..b23ca29
--- /dev/null
+++ b/talk/xmpp/moduleimpl.cc
@@ -0,0 +1,65 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#include "talk/base/common.h"
+#include "talk/xmpp/moduleimpl.h"
+
+namespace buzz {
+
+XmppModuleImpl::XmppModuleImpl() :
+  engine_(NULL),
+  stanza_handler_(this) {
+}
+
+XmppModuleImpl::~XmppModuleImpl()
+{
+  if (engine_ != NULL) {
+    engine_->RemoveStanzaHandler(&stanza_handler_);
+    engine_ = NULL;
+  }
+}
+
+XmppReturnStatus
+XmppModuleImpl::RegisterEngine(XmppEngine* engine)
+{
+  if (NULL == engine || NULL != engine_)
+    return XMPP_RETURN_BADARGUMENT;
+
+  engine->AddStanzaHandler(&stanza_handler_);
+  engine_ = engine;
+
+  return XMPP_RETURN_OK;
+}
+
+XmppEngine*
+XmppModuleImpl::engine() {
+  ASSERT(NULL != engine_);
+  return engine_;
+}
+
+}
+
diff --git a/talk/xmpp/moduleimpl.h b/talk/xmpp/moduleimpl.h
new file mode 100644
index 0000000..085c83a
--- /dev/null
+++ b/talk/xmpp/moduleimpl.h
@@ -0,0 +1,93 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#ifndef _moduleimpl_h_
+#define _moduleimpl_h_
+
+#include "talk/xmpp/xmppengine.h"
+#include "talk/xmpp/module.h"
+
+namespace buzz {
+
+//! This is the base implementation class for extension modules.
+//! An engine is registered with the module and the module then hooks the
+//! appropriate parts of the engine to implement that set of features.  It is
+//! important to unregister modules before destructing the engine.
+class XmppModuleImpl {
+protected:
+  XmppModuleImpl();
+  virtual ~XmppModuleImpl();
+
+  //! Register the engine with the module.  Only one engine can be associated
+  //! with a module at a time.  This method will return an error if there is
+  //! already an engine registered.
+  XmppReturnStatus RegisterEngine(XmppEngine* engine);
+
+  //! Gets the engine that this module is attached to.
+  XmppEngine* engine();
+
+  //! Process the given stanza.
+  //! The module must return true if it has handled the stanza.
+  //! A false return value causes the stanza to be passed on to
+  //! the next registered handler.
+  virtual bool HandleStanza(const XmlElement *) { return false; };
+
+private:
+
+  //! The ModuleSessionHelper nested class allows the Module
+  //! to hook into and get stanzas and events from the engine.
+  class ModuleStanzaHandler : public XmppStanzaHandler {
+    friend class XmppModuleImpl;
+
+    ModuleStanzaHandler(XmppModuleImpl* module) :
+      module_(module) {
+    }
+
+    bool HandleStanza(const XmlElement* stanza) {
+      return module_->HandleStanza(stanza);
+    }
+
+    XmppModuleImpl* module_;
+  };
+
+  friend class ModuleStanzaHandler;
+
+  XmppEngine* engine_;
+  ModuleStanzaHandler stanza_handler_;
+};
+
+
+// This macro will implement the XmppModule interface for a class
+// that derives from both XmppModuleImpl and XmppModule
+#define IMPLEMENT_XMPPMODULE \
+  XmppReturnStatus RegisterEngine(XmppEngine* engine) { \
+    return XmppModuleImpl::RegisterEngine(engine); \
+  }
+
+}
+
+#endif
diff --git a/talk/xmpp/mucroomhistorytask.cc b/talk/xmpp/mucroomhistorytask.cc
new file mode 100644
index 0000000..08c907a
--- /dev/null
+++ b/talk/xmpp/mucroomhistorytask.cc
@@ -0,0 +1,146 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+#include "talk/xmpp/mucroomhistorytask.h"
+#include "talk/xmpp/constants.h"
+
+namespace buzz {
+
+// TODO: Move these to xmpp/constants.cc once it's publicly
+// viewable.
+const char NS_GOOGLE_SETTING[] = "google:setting";
+const StaticQName QN_MEETING_HISTORY = { NS_GOOGLE_SETTING, "meetinghistory" };
+const StaticQName QN_MEETING_ITEM = { NS_GOOGLE_SETTING, "item" };
+
+MucRoomHistoryGetTask::MucRoomHistoryGetTask(XmppTaskParentInterface* parent,
+                                             const buzz::Jid& user_jid)
+    : IqTask(parent, STR_GET, user_jid, MakeRequest()) {
+}
+
+//    <iq type='get' id='user-setting-1'>
+//      <meetinghistory xmlns='google:setting' />
+//    </iq>
+XmlElement* MucRoomHistoryGetTask::MakeRequest() {
+  XmlElement* history = new XmlElement(QN_MEETING_HISTORY, true);
+  return history;
+}
+
+//   <iq type='result'
+//       to='romeo@gmail.com/orchard'
+//       id='user-setting-1'>
+//     <meetinghistory xmlns='google:setting'>
+//       <item jid=’private-muc-abc-...@groupchat.google.com’ name=’Hangout'
+//             time=’2011-01-31T13:20:00Z’ />
+//       <item jid=’private-muc-abc-...@groupchat.google.com’ name=’Planning’
+//             time=’2011-02-11T 14:20:00Z />
+//       <item jid=’private-muc-abc-...@groupchat.google.com’ name=’TGIF’
+//             time=’2011-03-21T15:20:00Z’ />
+//     </meetinghistory>
+//   </iq>
+void MucRoomHistoryGetTask::HandleResult(const XmlElement* stanza) {
+  std::vector<MucRoomHistoryInfo> meeting_rooms;
+
+  const XmlElement* history = stanza->FirstNamed(QN_MEETING_HISTORY);
+  if (history == NULL) {
+    SignalResult(meeting_rooms);
+    return;
+  }
+
+  const XmlElement* history_item = history->FirstNamed(QN_MEETING_ITEM);
+  while (history_item != NULL) {
+    MucRoomHistoryInfo room;
+    room.room_name = history_item->Attr(QN_NAME);
+    room.last_enter_time = history_item->Attr(QN_TIME);
+    meeting_rooms.push_back(room);
+
+    history_item = history_item->NextNamed(QN_MEETING_ITEM);
+  }
+
+  SignalResult(meeting_rooms);
+}
+
+MucRoomHistorySetTask::MucRoomHistorySetTask(XmppTaskParentInterface* parent,
+                                             MucRoomHistoryTaskCommand command,
+                                             const buzz::Jid& user_jid,
+                                             const std::string& room_name)
+    : IqTask(parent, STR_SET, user_jid, MakeRequest(command, room_name)) {
+}
+
+// ADD
+//   <iq type='set'
+//       to='romeo@gmail.com'
+//       id='user-setting-3'>
+//     <meetinghistory xmlns='google:setting'>
+//       <item jid=’private-muc-abc@groupchat.google.com’
+//             name=’Hangout' action=’add’ />
+//     </meetinghistory>
+//   </iq>
+
+// DELETE
+//   <iq type='set'
+//       to='romeo@gmail.com'
+//       id='user-setting-3'>
+//     <meetinghistory xmlns='google:setting'>
+//       <item jid=’private-muc-abc@groupchat.google.com’
+//             name=’Hangout' action=’remove’ />
+//     </meetinghistory>
+//   </iq>
+
+// CLEAR ALL
+//   <iq type='set'
+//       from='romeo@gmail.com/orchard'
+//       to='romeo@gmail.com'
+//       id='user-setting-3'>
+//     <meetinghistory xmlns='google:setting' />
+//   </iq>
+XmlElement* MucRoomHistorySetTask::MakeRequest(
+    MucRoomHistoryTaskCommand command, const std::string& room_name) {
+  XmlElement* history = new XmlElement(QN_MEETING_HISTORY, true);
+  if (command != HT_CLEAR_ALL) {
+    XmlElement* item = new XmlElement(QN_MEETING_ITEM, false);
+    // TODO - remove QN_JID attribute when 21446107 is submitted &
+    // deployed.  no harm to leave in after the change is made.
+    item->AddAttr(QN_JID, room_name);
+    item->AddAttr(QN_NAME, room_name);
+    item->AddAttr(QN_ACTION, command == HT_ADD ? "add" : "remove");
+
+    history->AddElement(item);
+  }
+
+  return history;
+}
+
+// MODIFY & CLEAR
+//   <iq type='result'
+//       to='romeo@gmail.com/orchard'
+//       from='romeo@gmail.com'
+//       id='user-setting-3' />
+void MucRoomHistorySetTask::HandleResult(const XmlElement* stanza) {
+  SignalResult();
+}
+
+}  // namespace buzz
diff --git a/talk/xmpp/mucroomhistorytask.h b/talk/xmpp/mucroomhistorytask.h
new file mode 100644
index 0000000..79d1bfa
--- /dev/null
+++ b/talk/xmpp/mucroomhistorytask.h
@@ -0,0 +1,75 @@
+/*
+ * libjingle
+ * Copyright 2011, 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.
+ */
+
+// https://docs.google.com/a/google.com/document/d/1PObnyJbdEWDOrgDZJFIH1peMvRoUkSTv6382zq3hbuM/edit?hl=en#
+
+#include <string>
+#include <vector>
+
+#include "talk/xmpp/iqtask.h"
+
+namespace buzz {
+
+struct MucRoomHistoryInfo {
+  std::string room_name;
+  std::string last_enter_time;
+};
+
+enum MucRoomHistoryTaskCommand {
+  HT_ADD = 0,
+  HT_CLEAR_ALL = 1,
+  HT_DELETE = 2,
+};
+
+class MucRoomHistoryGetTask : public IqTask {
+ public:
+  MucRoomHistoryGetTask(XmppTaskParentInterface* parent,
+                        const buzz::Jid& user_jid);
+
+  sigslot::signal1<const std::vector<MucRoomHistoryInfo>&> SignalResult;
+
+ protected:
+  static XmlElement* MakeRequest();
+  virtual void HandleResult(const XmlElement* stanza);
+};
+
+class MucRoomHistorySetTask : public IqTask {
+ public:
+  MucRoomHistorySetTask(XmppTaskParentInterface* parent,
+                        MucRoomHistoryTaskCommand command,
+                        const buzz::Jid& user_jid,
+                        const std::string& room_name);
+
+  sigslot::signal0<> SignalResult;
+
+ protected:
+  static XmlElement* MakeRequest(MucRoomHistoryTaskCommand command,
+                                 const std::string& room_name);
+  virtual void HandleResult(const XmlElement* stanza);
+};
+
+}  // namespace buzz
diff --git a/talk/xmpp/pubsub_task.cc b/talk/xmpp/pubsub_task.cc
new file mode 100644
index 0000000..97e0d4d
--- /dev/null
+++ b/talk/xmpp/pubsub_task.cc
@@ -0,0 +1,217 @@
+/*
+ * libjingle
+ * Copyright 2004--2006, 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.
+ */
+
+#include "talk/xmpp/pubsub_task.h"
+
+#include <map>
+#include <string>
+
+#include "talk/base/common.h"
+#include "talk/xmpp/constants.h"
+#include "talk/xmpp/xmppengine.h"
+
+namespace buzz {
+
+PubsubTask::PubsubTask(XmppTaskParentInterface* parent,
+                       const buzz::Jid& pubsub_node_jid)
+    : buzz::XmppTask(parent, buzz::XmppEngine::HL_SENDER),
+      pubsub_node_jid_(pubsub_node_jid) {
+}
+
+PubsubTask::~PubsubTask() {
+}
+
+// Checks for pubsub publish events as well as responses to get IQs.
+bool PubsubTask::HandleStanza(const buzz::XmlElement* stanza) {
+  const buzz::QName& stanza_name(stanza->Name());
+  if (stanza_name == buzz::QN_MESSAGE) {
+    if (MatchStanzaFrom(stanza, pubsub_node_jid_)) {
+      const buzz::XmlElement* pubsub_event_item =
+          stanza->FirstNamed(QN_PUBSUB_EVENT);
+      if (pubsub_event_item != NULL) {
+        QueueStanza(pubsub_event_item);
+        return true;
+      }
+    }
+  } else if (stanza_name == buzz::QN_IQ) {
+    if (MatchResponseIq(stanza, pubsub_node_jid_, task_id())) {
+      const buzz::XmlElement* pubsub_item = stanza->FirstNamed(QN_PUBSUB);
+      if (pubsub_item != NULL) {
+        QueueStanza(pubsub_item);
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+int PubsubTask::ProcessResponse() {
+  const buzz::XmlElement* stanza = NextStanza();
+  if (stanza == NULL) {
+    return STATE_BLOCKED;
+  }
+
+  if (stanza->Attr(buzz::QN_TYPE) == buzz::STR_ERROR) {
+    OnPubsubError(stanza->FirstNamed(buzz::QN_ERROR));
+    return STATE_RESPONSE;
+  }
+
+  const buzz::QName& stanza_name(stanza->Name());
+  if (stanza_name == QN_PUBSUB_EVENT) {
+    HandlePubsubEventMessage(stanza);
+  } else if (stanza_name == QN_PUBSUB) {
+    HandlePubsubIqGetResponse(stanza);
+  }
+
+  return STATE_RESPONSE;
+}
+
+// Registers a function pointer to be called when the value of the pubsub
+// node changes.
+// Note that this does not actually change the XMPP pubsub
+// subscription. All publish events are always received by everyone in the
+// MUC. This function just controls whether the handle function will get
+// called when the event is received.
+bool PubsubTask::SubscribeToNode(const std::string& pubsub_node,
+                                 NodeHandler handler) {
+  subscribed_nodes_[pubsub_node] = handler;
+  talk_base::scoped_ptr<buzz::XmlElement> get_iq_request(
+      MakeIq(buzz::STR_GET, pubsub_node_jid_, task_id()));
+  if (get_iq_request.get() == NULL) {
+    return false;
+  }
+  buzz::XmlElement* pubsub_element = new buzz::XmlElement(QN_PUBSUB, true);
+  buzz::XmlElement* items_element = new buzz::XmlElement(QN_PUBSUB_ITEMS, true);
+
+  items_element->AddAttr(buzz::QN_NODE, pubsub_node);
+  pubsub_element->AddElement(items_element);
+  get_iq_request->AddElement(pubsub_element);
+
+  if (SendStanza(get_iq_request.get()) != buzz::XMPP_RETURN_OK) {
+    return false;
+  }
+
+  return true;
+}
+
+void PubsubTask::UnsubscribeFromNode(const std::string& pubsub_node) {
+  subscribed_nodes_.erase(pubsub_node);
+}
+
+void PubsubTask::OnPubsubError(const buzz::XmlElement* error_stanza) {
+}
+
+// Checks for a pubsub event message like the following:
+//
+//  <message from="muvc-private-chat-some-id@groupchat.google.com"
+//   to="john@site.com/gcomm582B14C9">
+//    <event xmlns:"http://jabber.org/protocol/pubsub#event">
+//      <items node="node-name">
+//        <item id="some-id">
+//          <payload/>
+//        </item>
+//      </items>
+//    </event>
+//  </message>
+//
+// It also checks for retraction event messages like the following:
+//
+//  <message from="muvc-private-chat-some-id@groupchat.google.com"
+//   to="john@site.com/gcomm582B14C9">
+//    <event xmlns:"http://jabber.org/protocol/pubsub#event">
+//      <items node="node-name">
+//        <retract id="some-id"/>
+//      </items>
+//    </event>
+//  </message>
+void PubsubTask::HandlePubsubEventMessage(
+    const buzz::XmlElement* pubsub_event) {
+  ASSERT(pubsub_event->Name() == QN_PUBSUB_EVENT);
+  for (const buzz::XmlChild* child = pubsub_event->FirstChild();
+       child != NULL;
+       child = child->NextChild()) {
+    const buzz::XmlElement* child_element = child->AsElement();
+    const buzz::QName& child_name(child_element->Name());
+    if (child_name == QN_PUBSUB_EVENT_ITEMS) {
+      HandlePubsubItems(child_element);
+    }
+  }
+}
+
+// Checks for a response to an pubsub IQ get like the following:
+//
+//  <iq from="muvc-private-chat-some-id@groupchat.google.com"
+//   to="john@site.com/gcomm582B14C9"
+//   type="result">
+//    <pubsub xmlns:"http://jabber.org/protocol/pubsub">
+//      <items node="node-name">
+//        <item id="some-id">
+//          <payload/>
+//        </item>
+//      </items>
+//    </event>
+//  </message>
+void PubsubTask::HandlePubsubIqGetResponse(
+    const buzz::XmlElement* pubsub_iq_response) {
+  ASSERT(pubsub_iq_response->Name() == QN_PUBSUB);
+  for (const buzz::XmlChild* child = pubsub_iq_response->FirstChild();
+       child != NULL;
+       child = child->NextChild()) {
+    const buzz::XmlElement* child_element = child->AsElement();
+    const buzz::QName& child_name(child_element->Name());
+    if (child_name == QN_PUBSUB_ITEMS) {
+      HandlePubsubItems(child_element);
+    }
+  }
+}
+
+// Calls registered handlers in response to pubsub event or response to
+// IQ pubsub get.
+// 'items' is the child of a pubsub#event:event node or pubsub:pubsub node.
+void PubsubTask::HandlePubsubItems(const buzz::XmlElement* items) {
+  ASSERT(items->HasAttr(QN_NODE));
+  const std::string& node_name(items->Attr(QN_NODE));
+  NodeSubscriptions::iterator iter = subscribed_nodes_.find(node_name);
+  if (iter != subscribed_nodes_.end()) {
+    NodeHandler handler = iter->second;
+    const buzz::XmlElement* item = items->FirstElement();
+    while (item != NULL) {
+      const buzz::QName& item_name(item->Name());
+      if (item_name != QN_PUBSUB_EVENT_ITEM &&
+          item_name != QN_PUBSUB_EVENT_RETRACT &&
+          item_name != QN_PUBSUB_ITEM) {
+        continue;
+      }
+
+      (this->*handler)(item);
+      item = item->NextElement();
+    }
+    return;
+  }
+}
+
+}
diff --git a/talk/xmpp/pubsub_task.h b/talk/xmpp/pubsub_task.h
new file mode 100644
index 0000000..45a7462
--- /dev/null
+++ b/talk/xmpp/pubsub_task.h
@@ -0,0 +1,75 @@
+/*
+ * libjingle
+ * Copyright 2004--2011, 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.
+ */
+
+#ifndef TALK_XMPP_PUBSUB_TASK_H_
+#define TALK_XMPP_PUBSUB_TASK_H_
+
+#include <map>
+#include <string>
+#include "talk/xmllite/xmlelement.h"
+#include "talk/xmpp/jid.h"
+#include "talk/xmpp/xmpptask.h"
+
+namespace buzz {
+
+// Base class to help write pubsub tasks.
+// In ProcessStart call SubscribeNode with namespaces of interest along with
+// NodeHandlers.
+// When pubsub notifications arrive and matches the namespace, the NodeHandlers
+// will be called back.
+class PubsubTask : public buzz::XmppTask {
+ public:
+  virtual ~PubsubTask();
+
+ protected:
+  typedef void (PubsubTask::*NodeHandler)(const buzz::XmlElement* node);
+
+  PubsubTask(XmppTaskParentInterface* parent, const buzz::Jid& pubsub_node_jid);
+
+  virtual bool HandleStanza(const buzz::XmlElement* stanza);
+  virtual int ProcessResponse();
+
+  bool SubscribeToNode(const std::string& pubsub_node, NodeHandler handler);
+  void UnsubscribeFromNode(const std::string& pubsub_node);
+
+  // Called when there is an error. Derived class can do what it needs to.
+  virtual void OnPubsubError(const buzz::XmlElement* error_stanza);
+
+ private:
+  typedef std::map<std::string, NodeHandler> NodeSubscriptions;
+
+  void HandlePubsubIqGetResponse(const buzz::XmlElement* pubsub_iq_response);
+  void HandlePubsubEventMessage(const buzz::XmlElement* pubsub_event_message);
+  void HandlePubsubItems(const buzz::XmlElement* items);
+
+  buzz::Jid pubsub_node_jid_;
+  NodeSubscriptions subscribed_nodes_;
+};
+
+}  // namespace buzz
+
+#endif // TALK_XMPP_PUBSUB_TASK_H_
diff --git a/talk/xmpp/rostermodule.h b/talk/xmpp/rostermodule.h
new file mode 100644
index 0000000..e81c4f0
--- /dev/null
+++ b/talk/xmpp/rostermodule.h
@@ -0,0 +1,325 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#ifndef _rostermodule_h_
+#define _rostermodule_h_
+
+#include "talk/xmpp/module.h"
+
+namespace buzz {
+
+class XmppRosterModule;
+
+// The main way you initialize and use the module would be like this:
+//    XmppRosterModule *roster_module = XmppRosterModule::Create();
+//    roster_module->RegisterEngine(engine);
+//    roster_module->BroadcastPresence();
+//    roster_module->RequestRosterUpdate();
+
+//! This enum captures the valid values for the show attribute in a presence
+//! stanza
+enum XmppPresenceShow
+{
+  XMPP_PRESENCE_CHAT = 0,
+  XMPP_PRESENCE_DEFAULT = 1,
+  XMPP_PRESENCE_AWAY = 2,
+  XMPP_PRESENCE_XA = 3,
+  XMPP_PRESENCE_DND = 4,
+};
+
+//! These are the valid subscription states in a roster contact.  This
+//! represents the combination of the subscription and ask attributes
+enum XmppSubscriptionState
+{
+  XMPP_SUBSCRIPTION_NONE = 0,
+  XMPP_SUBSCRIPTION_NONE_ASKED = 1,
+  XMPP_SUBSCRIPTION_TO = 2,
+  XMPP_SUBSCRIPTION_FROM = 3,
+  XMPP_SUBSCRIPTION_FROM_ASKED = 4,
+  XMPP_SUBSCRIPTION_BOTH = 5,
+};
+
+//! These represent the valid types of presence stanzas for managing
+//! subscriptions
+enum XmppSubscriptionRequestType
+{
+  XMPP_REQUEST_SUBSCRIBE = 0,
+  XMPP_REQUEST_UNSUBSCRIBE = 1,
+  XMPP_REQUEST_SUBSCRIBED = 2,
+  XMPP_REQUEST_UNSUBSCRIBED = 3,
+};
+
+enum XmppPresenceAvailable {
+  XMPP_PRESENCE_UNAVAILABLE = 0,
+  XMPP_PRESENCE_AVAILABLE   = 1,
+  XMPP_PRESENCE_ERROR       = 2,
+};
+
+//! Presence Information
+//! This class stores both presence information for outgoing presence and is
+//! returned by methods in XmppRosterModule to represent recieved incoming
+//! presence information.  When this class is writeable (non-const) then each
+//! update to any property will set the inner xml.  Setting the raw_xml will
+//! rederive all of the other properties.
+class XmppPresence {
+public:
+  virtual ~XmppPresence() {}
+
+  //! Create a new Presence
+  //! This is typically only used when sending a directed presence
+  static XmppPresence* Create();
+
+  //! The Jid of for the presence information.
+  //! Typically this will be a full Jid with resource specified.
+  virtual const Jid jid() const = 0;
+
+  //! Is the contact available?
+  virtual XmppPresenceAvailable available() const = 0;
+
+  //! Sets if the user is available or not
+  virtual XmppReturnStatus set_available(XmppPresenceAvailable available) = 0;
+
+  //! The show value of the presence info
+  virtual XmppPresenceShow presence_show() const = 0;
+
+  //! Set the presence show value
+  virtual XmppReturnStatus set_presence_show(XmppPresenceShow show) = 0;
+
+  //! The Priority of the presence info
+  virtual int priority() const = 0;
+
+  //! Set the priority of the presence
+  virtual XmppReturnStatus set_priority(int priority) = 0;
+
+  //! The plain text status of the presence info.
+  //! If there are multiple status because of language, this will either be a
+  //! status that is not tagged for language or the first available
+  virtual const std::string& status() const = 0;
+
+  //! Sets the status for the presence info.
+  //! If there is more than one status present already then this will remove
+  //! them all and replace it with one status element we no specified language
+  virtual XmppReturnStatus set_status(const std::string& status) = 0;
+
+  //! The raw xml of the presence update
+  virtual const XmlElement* raw_xml() const = 0;
+
+  //! Sets the raw presence stanza for the presence update
+  //! This will cause all other data items in this structure to be rederived
+  virtual XmppReturnStatus set_raw_xml(const XmlElement * xml) = 0;
+};
+
+//! A contact as given by the server
+class XmppRosterContact {
+public:
+  virtual ~XmppRosterContact() {}
+
+  //! Create a new roster contact
+  //! This is typically only used when doing a roster update/add
+  static XmppRosterContact* Create();
+
+  //! The jid for the contact.
+  //! Typically this will be a bare Jid.
+  virtual const Jid jid() const = 0;
+
+  //! Sets the jid for the roster contact update
+  virtual XmppReturnStatus set_jid(const Jid& jid) = 0;
+
+  //! The name (nickname) stored for this contact
+  virtual const std::string& name() const = 0;
+
+  //! Sets the name
+  virtual XmppReturnStatus set_name(const std::string& name) = 0;
+
+  //! The Presence subscription state stored on the server for this contact
+  //! This is never settable and will be ignored when generating a roster
+  //! add/update request
+  virtual XmppSubscriptionState subscription_state() const = 0;
+
+  //! The number of Groups applied to this contact
+  virtual size_t GetGroupCount() const = 0;
+
+  //! Gets a Group applied to the contact based on index.
+  //! range
+  virtual const std::string& GetGroup(size_t index) const = 0;
+
+  //! Adds a group to this contact.
+  //! This will return a bad argument error if the group is already there.
+  virtual XmppReturnStatus AddGroup(const std::string& group) = 0;
+
+  //! Removes a group from the contact.
+  //! This will return an error if the group cannot be found in the group list.
+  virtual XmppReturnStatus RemoveGroup(const std::string& group) = 0;
+
+  //! The raw xml for this roster contact
+  virtual const XmlElement* raw_xml() const = 0;
+
+  //! Sets the raw presence stanza for the contact update/add
+  //! This will cause all other data items in this structure to be rederived
+  virtual XmppReturnStatus set_raw_xml(const XmlElement * xml) = 0;
+};
+
+//! The XmppRosterHandler is an interface for callbacks from the module
+class XmppRosterHandler {
+public:
+  //! A request for a subscription has come in.
+  //! Typically, the UI will ask the user if it is okay to let the requester
+  //! get presence notifications for the user.  The response is send back
+  //! by calling ApproveSubscriber or CancelSubscriber.
+  virtual void SubscriptionRequest(XmppRosterModule* roster,
+                                   const Jid& requesting_jid,
+                                   XmppSubscriptionRequestType type,
+                                   const XmlElement* raw_xml) = 0;
+
+  //! Some type of presence error has occured
+  virtual void SubscriptionError(XmppRosterModule* roster,
+                                 const Jid& from,
+                                 const XmlElement* raw_xml) = 0;
+
+  virtual void RosterError(XmppRosterModule* roster,
+                           const XmlElement* raw_xml) = 0;
+
+  //! New presence information has come in
+  //! The user is notified with the presence object directly.  This info is also
+  //! added to the store accessable from the engine.
+  virtual void IncomingPresenceChanged(XmppRosterModule* roster,
+                                       const XmppPresence* presence) = 0;
+
+  //! A contact has changed
+  //! This indicates that the data for a contact may have changed.  No
+  //! contacts have been added or removed.
+  virtual void ContactChanged(XmppRosterModule* roster,
+                              const XmppRosterContact* old_contact,
+                              size_t index) = 0;
+
+  //! A set of contacts have been added
+  //! These contacts may have been added in response to the original roster
+  //! request or due to a "roster push" from the server.
+  virtual void ContactsAdded(XmppRosterModule* roster,
+                             size_t index, size_t number) = 0;
+
+  //! A contact has been removed
+  //! This contact has been removed form the list.
+  virtual void ContactRemoved(XmppRosterModule* roster,
+                              const XmppRosterContact* removed_contact,
+                              size_t index) = 0;
+
+};
+
+//! An XmppModule for handle roster and presence functionality
+class XmppRosterModule : public XmppModule {
+public:
+  //! Creates a new XmppRosterModule
+  static XmppRosterModule * Create();
+  virtual ~XmppRosterModule() {}
+
+  //! Sets the roster handler (callbacks) for the module
+  virtual XmppReturnStatus set_roster_handler(XmppRosterHandler * handler) = 0;
+
+  //! Gets the roster handler for the module
+  virtual XmppRosterHandler* roster_handler() = 0;
+
+  // USER PRESENCE STATE -------------------------------------------------------
+
+  //! Gets the aggregate outgoing presence
+  //! This object is non-const and be edited directly.  No update is sent
+  //! to the server until a Broadcast is sent
+  virtual XmppPresence* outgoing_presence() = 0;
+
+  //! Broadcasts that the user is available.
+  //! Nothing with respect to presence is sent until this is called.
+  virtual XmppReturnStatus BroadcastPresence() = 0;
+
+  //! Sends a directed presence to a Jid
+  //! Note that the client doesn't store where directed presence notifications
+  //! have been sent.  The server can keep the appropriate state
+  virtual XmppReturnStatus SendDirectedPresence(const XmppPresence* presence,
+                                                const Jid& to_jid) = 0;
+
+  // INCOMING PRESENCE STATUS --------------------------------------------------
+
+  //! Returns the number of incoming presence data recorded
+  virtual size_t GetIncomingPresenceCount() = 0;
+
+  //! Returns an incoming presence datum based on index
+  virtual const XmppPresence* GetIncomingPresence(size_t index) = 0;
+
+  //! Gets the number of presence data for a bare Jid
+  //! There may be a datum per resource
+  virtual size_t GetIncomingPresenceForJidCount(const Jid& jid) = 0;
+
+  //! Returns a single presence data for a Jid based on index
+  virtual const XmppPresence* GetIncomingPresenceForJid(const Jid& jid,
+                                                        size_t index) = 0;
+
+  // ROSTER MANAGEMENT ---------------------------------------------------------
+
+  //! Requests an update of the roster from the server
+  //! This must be called to initialize the client side cache of the roster
+  //! After this is sent the server should keep this module apprised of any
+  //! changes.
+  virtual XmppReturnStatus RequestRosterUpdate() = 0;
+
+  //! Returns the number of contacts in the roster
+  virtual size_t GetRosterContactCount() = 0;
+
+  //! Returns a contact by index
+  virtual const XmppRosterContact* GetRosterContact(size_t index) = 0;
+
+  //! Finds a contact by Jid
+  virtual const XmppRosterContact* FindRosterContact(const Jid& jid) = 0;
+
+  //! Send a request to the server to add a contact
+  //! Note that the contact won't show up in the roster until the server can
+  //! respond.  This happens async when the socket is being serviced
+  virtual XmppReturnStatus RequestRosterChange(
+    const XmppRosterContact* contact) = 0;
+
+  //! Request that the server remove a contact
+  //! The jabber protocol specifies that the server should also cancel any
+  //! subscriptions when this is done.  Like adding, this contact won't be
+  //! removed until the server responds.
+  virtual XmppReturnStatus RequestRosterRemove(const Jid& jid) = 0;
+
+  // SUBSCRIPTION MANAGEMENT ---------------------------------------------------
+
+  //! Request a subscription to presence notifications form a Jid
+  virtual XmppReturnStatus RequestSubscription(const Jid& jid) = 0;
+
+  //! Cancel a subscription to presence notifications from a Jid
+  virtual XmppReturnStatus CancelSubscription(const Jid& jid) = 0;
+
+  //! Approve a request to deliver presence notifications to a jid
+  virtual XmppReturnStatus ApproveSubscriber(const Jid& jid) = 0;
+
+  //! Deny or cancel presence notification deliver to a jid
+  virtual XmppReturnStatus CancelSubscriber(const Jid& jid) = 0;
+};
+
+}
+
+#endif
diff --git a/talk/xmpp/rostermoduleimpl.cc b/talk/xmpp/rostermoduleimpl.cc
new file mode 100644
index 0000000..c5da5a9
--- /dev/null
+++ b/talk/xmpp/rostermoduleimpl.cc
@@ -0,0 +1,1041 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#include <vector>
+#include <string>
+#include <map>
+#include <algorithm>
+#include <sstream>
+#include <iostream>
+#include "talk/base/common.h"
+#include "talk/base/stringencode.h"
+#include "talk/xmpp/constants.h"
+#include "talk/xmpp/rostermoduleimpl.h"
+
+namespace buzz {
+
+// enum prase and persist helpers ----------------------------------------------
+static bool
+StringToPresenceShow(const std::string& input, XmppPresenceShow* show)
+{
+  // If this becomes a perf issue we can use a hash or a map here
+  if (STR_SHOW_AWAY == input)
+    *show = XMPP_PRESENCE_AWAY;
+  else if (STR_SHOW_DND == input)
+    *show = XMPP_PRESENCE_DND;
+  else if (STR_SHOW_XA == input)
+    *show = XMPP_PRESENCE_XA;
+  else if (STR_SHOW_CHAT == input)
+    *show = XMPP_PRESENCE_CHAT;
+  else if (STR_EMPTY == input)
+    *show = XMPP_PRESENCE_DEFAULT;
+  else
+    return false;
+
+  return true;
+}
+
+static bool
+PresenceShowToString(XmppPresenceShow show, const std::string ** output)
+{
+  switch(show) {
+    case XMPP_PRESENCE_AWAY:
+      *output = &STR_SHOW_AWAY;
+      return true;
+    case XMPP_PRESENCE_CHAT:
+      *output = &STR_SHOW_CHAT;
+      return true;
+    case XMPP_PRESENCE_XA:
+      *output = &STR_SHOW_XA;
+      return true;
+    case XMPP_PRESENCE_DND:
+      *output = &STR_SHOW_DND;
+      return true;
+    case XMPP_PRESENCE_DEFAULT:
+      *output = &STR_EMPTY;
+      return true;
+  }
+
+  *output = &STR_EMPTY;
+  return false;
+}
+
+static bool
+StringToSubscriptionState(const std::string& subscription,
+                          const std::string& ask,
+                          XmppSubscriptionState* state)
+{
+  if (ask == "subscribe")
+  {
+    if (subscription == "none") {
+      *state = XMPP_SUBSCRIPTION_NONE_ASKED;
+      return true;
+    }
+    if (subscription == "from") {
+      *state = XMPP_SUBSCRIPTION_FROM_ASKED;
+      return true;
+    }
+  } else if (ask == STR_EMPTY)
+  {
+    if (subscription == "none") {
+      *state = XMPP_SUBSCRIPTION_NONE;
+      return true;
+    }
+    if (subscription == "from") {
+      *state = XMPP_SUBSCRIPTION_FROM;
+      return true;
+    }
+    if (subscription == "to") {
+      *state = XMPP_SUBSCRIPTION_TO;
+      return true;
+    }
+    if (subscription == "both") {
+      *state = XMPP_SUBSCRIPTION_BOTH;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+static bool
+StringToSubscriptionRequestType(const std::string& string,
+                                XmppSubscriptionRequestType* type)
+{
+  if (string == "subscribe")
+    *type = XMPP_REQUEST_SUBSCRIBE;
+  else if (string == "unsubscribe")
+    *type = XMPP_REQUEST_UNSUBSCRIBE;
+  else if (string == "subscribed")
+    *type = XMPP_REQUEST_SUBSCRIBED;
+  else if (string == "unsubscribed")
+    *type = XMPP_REQUEST_UNSUBSCRIBED;
+  else
+    return false;
+  return true;
+}
+
+// XmppPresenceImpl class ------------------------------------------------------
+XmppPresence*
+XmppPresence::Create() {
+  return new XmppPresenceImpl();
+}
+
+XmppPresenceImpl::XmppPresenceImpl() {
+}
+
+const Jid
+XmppPresenceImpl::jid() const {
+  if (!raw_xml_.get())
+    return JID_EMPTY;
+
+  return Jid(raw_xml_->Attr(QN_FROM));
+}
+
+XmppPresenceAvailable
+XmppPresenceImpl::available() const {
+  if (!raw_xml_.get())
+    return XMPP_PRESENCE_UNAVAILABLE;
+
+  if (raw_xml_->Attr(QN_TYPE) == "unavailable")
+    return XMPP_PRESENCE_UNAVAILABLE;
+  else if (raw_xml_->Attr(QN_TYPE) == "error")
+    return XMPP_PRESENCE_ERROR;
+  else
+    return XMPP_PRESENCE_AVAILABLE;
+}
+
+XmppReturnStatus
+XmppPresenceImpl::set_available(XmppPresenceAvailable available) {
+  if (!raw_xml_.get())
+    CreateRawXmlSkeleton();
+
+  if (available == XMPP_PRESENCE_AVAILABLE)
+    raw_xml_->ClearAttr(QN_TYPE);
+  else if (available == XMPP_PRESENCE_UNAVAILABLE)
+    raw_xml_->SetAttr(QN_TYPE, "unavailable");
+  else if (available == XMPP_PRESENCE_ERROR)
+    raw_xml_->SetAttr(QN_TYPE, "error");
+  return XMPP_RETURN_OK;
+}
+
+XmppPresenceShow
+XmppPresenceImpl::presence_show() const {
+  if (!raw_xml_.get())
+    return XMPP_PRESENCE_DEFAULT;
+
+  XmppPresenceShow show;
+
+  if (StringToPresenceShow(raw_xml_->TextNamed(QN_SHOW), &show))
+    return show;
+
+  return XMPP_PRESENCE_DEFAULT;
+}
+
+XmppReturnStatus
+XmppPresenceImpl::set_presence_show(XmppPresenceShow show) {
+  if (!raw_xml_.get())
+    CreateRawXmlSkeleton();
+
+  const std::string* show_string;
+
+  if(!PresenceShowToString(show, &show_string))
+    return XMPP_RETURN_BADARGUMENT;
+
+  raw_xml_->ClearNamedChildren(QN_SHOW);
+
+  if (show!=XMPP_PRESENCE_DEFAULT) {
+    raw_xml_->AddElement(new XmlElement(QN_SHOW));
+    raw_xml_->AddText(*show_string, 1);
+  }
+
+  return XMPP_RETURN_OK;
+}
+
+int
+XmppPresenceImpl::priority() const {
+  if (!raw_xml_.get())
+    return 0;
+
+  int raw_priority;
+  if (!talk_base::FromString(raw_xml_->TextNamed(QN_PRIORITY), &raw_priority))
+    raw_priority = 0;
+  if (raw_priority < -128)
+    raw_priority = -128;
+  if (raw_priority > 127)
+    raw_priority = 127;
+
+  return raw_priority;
+}
+
+XmppReturnStatus
+XmppPresenceImpl::set_priority(int priority) {
+  if (!raw_xml_.get())
+    CreateRawXmlSkeleton();
+
+  if (priority < -128 || priority > 127)
+    return XMPP_RETURN_BADARGUMENT;
+
+  raw_xml_->ClearNamedChildren(QN_PRIORITY);
+  if (0 != priority) {
+    std::string priority_string;
+    if (talk_base::ToString(priority, &priority_string)) {
+      raw_xml_->AddElement(new XmlElement(QN_PRIORITY));
+      raw_xml_->AddText(priority_string, 1);
+    }
+  }
+
+  return XMPP_RETURN_OK;
+}
+
+const std::string&
+XmppPresenceImpl::status() const {
+  if (!raw_xml_.get())
+    return STR_EMPTY;
+
+  XmlElement* status_element;
+  XmlElement* element;
+
+  // Search for a status element with no xml:lang attribute on it.  if we can't
+  // find that then just return the first status element in the stanza.
+  for (status_element = element = raw_xml_->FirstNamed(QN_STATUS);
+       element;
+       element = element->NextNamed(QN_STATUS)) {
+    if (!element->HasAttr(QN_XML_LANG)) {
+      status_element = element;
+      break;
+    }
+  }
+
+  if (status_element) {
+    return status_element->BodyText();
+  }
+
+  return STR_EMPTY;
+}
+
+XmppReturnStatus
+XmppPresenceImpl::set_status(const std::string& status) {
+  if (!raw_xml_.get())
+    CreateRawXmlSkeleton();
+
+  raw_xml_->ClearNamedChildren(QN_STATUS);
+
+  if (status != STR_EMPTY) {
+    raw_xml_->AddElement(new XmlElement(QN_STATUS));
+    raw_xml_->AddText(status, 1);
+  }
+
+  return XMPP_RETURN_OK;
+}
+
+const XmlElement*
+XmppPresenceImpl::raw_xml() const {
+  if (!raw_xml_.get())
+    const_cast<XmppPresenceImpl*>(this)->CreateRawXmlSkeleton();
+  return raw_xml_.get();
+}
+
+XmppReturnStatus
+XmppPresenceImpl::set_raw_xml(const XmlElement * xml) {
+  if (!xml ||
+      xml->Name() != QN_PRESENCE)
+    return XMPP_RETURN_BADARGUMENT;
+
+  const std::string& type = xml->Attr(QN_TYPE);
+  if (type != STR_EMPTY && type != "unavailable")
+    return XMPP_RETURN_BADARGUMENT;
+
+  raw_xml_.reset(new XmlElement(*xml));
+
+  return XMPP_RETURN_OK;
+}
+
+void
+XmppPresenceImpl::CreateRawXmlSkeleton() {
+  raw_xml_.reset(new XmlElement(QN_PRESENCE));
+}
+
+// XmppRosterContactImpl -------------------------------------------------------
+XmppRosterContact*
+XmppRosterContact::Create() {
+  return new XmppRosterContactImpl();
+}
+
+XmppRosterContactImpl::XmppRosterContactImpl() {
+  ResetGroupCache();
+}
+
+void
+XmppRosterContactImpl::SetXmlFromWire(const XmlElement* xml) {
+  ResetGroupCache();
+  if (xml)
+    raw_xml_.reset(new XmlElement(*xml));
+  else
+    raw_xml_.reset(NULL);
+}
+
+void
+XmppRosterContactImpl::ResetGroupCache() {
+  group_count_ = -1;
+  group_index_returned_ = -1;
+  group_returned_ = NULL;
+}
+
+const Jid
+XmppRosterContactImpl::jid() const {
+  return Jid(raw_xml_->Attr(QN_JID));
+}
+
+XmppReturnStatus
+XmppRosterContactImpl::set_jid(const Jid& jid)
+{
+  if (!raw_xml_.get())
+    CreateRawXmlSkeleton();
+
+  if (!jid.IsValid())
+    return XMPP_RETURN_BADARGUMENT;
+
+  raw_xml_->SetAttr(QN_JID, jid.Str());
+
+  return XMPP_RETURN_OK;
+}
+
+const std::string&
+XmppRosterContactImpl::name() const {
+  return raw_xml_->Attr(QN_NAME);
+}
+
+XmppReturnStatus
+XmppRosterContactImpl::set_name(const std::string& name) {
+  if (!raw_xml_.get())
+    CreateRawXmlSkeleton();
+
+  if (name == STR_EMPTY)
+    raw_xml_->ClearAttr(QN_NAME);
+  else
+    raw_xml_->SetAttr(QN_NAME, name);
+
+  return XMPP_RETURN_OK;
+}
+
+XmppSubscriptionState
+XmppRosterContactImpl::subscription_state() const {
+  if (!raw_xml_.get())
+    return XMPP_SUBSCRIPTION_NONE;
+
+  XmppSubscriptionState state;
+
+  if (StringToSubscriptionState(raw_xml_->Attr(QN_SUBSCRIPTION),
+                                raw_xml_->Attr(QN_ASK),
+                                &state))
+    return state;
+
+  return XMPP_SUBSCRIPTION_NONE;
+}
+
+size_t
+XmppRosterContactImpl::GetGroupCount() const {
+  if (!raw_xml_.get())
+    return 0;
+
+  if (-1 == group_count_) {
+    XmlElement *group_element = raw_xml_->FirstNamed(QN_ROSTER_GROUP);
+    int group_count = 0;
+    while(group_element) {
+      group_count++;
+      group_element = group_element->NextNamed(QN_ROSTER_GROUP);
+    }
+
+    ASSERT(group_count > 0); // protect the cast
+    XmppRosterContactImpl * me = const_cast<XmppRosterContactImpl*>(this);
+    me->group_count_ = group_count;
+  }
+
+  return group_count_;
+}
+
+const std::string&
+XmppRosterContactImpl::GetGroup(size_t index) const {
+  if (index >= GetGroupCount())
+    return STR_EMPTY;
+
+  // We cache the last group index and element that we returned.  This way
+  // going through the groups in order is order n and not n^2.  This could be
+  // enhanced if necessary by starting at the cached value if the index asked
+  // is after the cached one.
+  if (group_index_returned_ >= 0 &&
+      index == static_cast<size_t>(group_index_returned_) + 1)
+  {
+    XmppRosterContactImpl * me = const_cast<XmppRosterContactImpl*>(this);
+    me->group_returned_ = group_returned_->NextNamed(QN_ROSTER_GROUP);
+    ASSERT(group_returned_ != NULL);
+    me->group_index_returned_++;
+  } else if (group_index_returned_ < 0 ||
+             static_cast<size_t>(group_index_returned_) != index) {
+    XmlElement * group_element = raw_xml_->FirstNamed(QN_ROSTER_GROUP);
+    size_t group_index = 0;
+    while(group_index < index) {
+      ASSERT(group_element != NULL);
+      group_index++;
+      group_element = group_element->NextNamed(QN_ROSTER_GROUP);
+    }
+
+    XmppRosterContactImpl * me = const_cast<XmppRosterContactImpl*>(this);
+    me->group_index_returned_ = static_cast<int>(group_index);
+    me->group_returned_ = group_element;
+  }
+
+  return group_returned_->BodyText();
+}
+
+XmppReturnStatus
+XmppRosterContactImpl::AddGroup(const std::string& group) {
+  if (group == STR_EMPTY)
+    return XMPP_RETURN_BADARGUMENT;
+
+  if (!raw_xml_.get())
+    CreateRawXmlSkeleton();
+
+  if (FindGroup(group, NULL, NULL))
+    return XMPP_RETURN_OK;
+
+  raw_xml_->AddElement(new XmlElement(QN_ROSTER_GROUP));
+  raw_xml_->AddText(group, 1);
+  ++group_count_;
+
+  return XMPP_RETURN_OK;
+}
+
+XmppReturnStatus
+XmppRosterContactImpl::RemoveGroup(const std::string& group) {
+  if (group == STR_EMPTY)
+    return XMPP_RETURN_BADARGUMENT;
+
+  if (!raw_xml_.get())
+    return XMPP_RETURN_OK;
+
+  XmlChild * child_before;
+  if (FindGroup(group, NULL, &child_before)) {
+    raw_xml_->RemoveChildAfter(child_before);
+    ResetGroupCache();
+  }
+  return XMPP_RETURN_OK;
+}
+
+bool
+XmppRosterContactImpl::FindGroup(const std::string& group,
+                                 XmlElement** element,
+                                 XmlChild** child_before) {
+  XmlChild * prev_child = NULL;
+  XmlChild * next_child;
+  XmlChild * child;
+  for (child = raw_xml_->FirstChild(); child; child = next_child) {
+    next_child = child->NextChild();
+    if (!child->IsText() &&
+        child->AsElement()->Name() == QN_ROSTER_GROUP &&
+        child->AsElement()->BodyText() == group) {
+      if (element)
+        *element = child->AsElement();
+      if (child_before)
+        *child_before = prev_child;
+      return true;
+    }
+    prev_child = child;
+  }
+
+  return false;
+}
+
+const XmlElement*
+XmppRosterContactImpl::raw_xml() const {
+  if (!raw_xml_.get())
+    const_cast<XmppRosterContactImpl*>(this)->CreateRawXmlSkeleton();
+  return raw_xml_.get();
+}
+
+XmppReturnStatus
+XmppRosterContactImpl::set_raw_xml(const XmlElement* xml) {
+  if (!xml ||
+      xml->Name() != QN_ROSTER_ITEM ||
+      xml->HasAttr(QN_SUBSCRIPTION) ||
+      xml->HasAttr(QN_ASK))
+    return XMPP_RETURN_BADARGUMENT;
+
+  ResetGroupCache();
+
+  raw_xml_.reset(new XmlElement(*xml));
+
+  return XMPP_RETURN_OK;
+}
+
+void
+XmppRosterContactImpl::CreateRawXmlSkeleton() {
+  raw_xml_.reset(new XmlElement(QN_ROSTER_ITEM));
+}
+
+// XmppRosterModuleImpl --------------------------------------------------------
+XmppRosterModule *
+XmppRosterModule::Create() {
+  return new XmppRosterModuleImpl();
+}
+
+XmppRosterModuleImpl::XmppRosterModuleImpl() :
+  roster_handler_(NULL),
+  incoming_presence_map_(new JidPresenceVectorMap()),
+  incoming_presence_vector_(new PresenceVector()),
+  contacts_(new ContactVector()) {
+
+}
+
+XmppRosterModuleImpl::~XmppRosterModuleImpl() {
+  DeleteIncomingPresence();
+  DeleteContacts();
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::set_roster_handler(XmppRosterHandler * handler) {
+  roster_handler_ = handler;
+  return XMPP_RETURN_OK;
+}
+
+XmppRosterHandler*
+XmppRosterModuleImpl::roster_handler() {
+  return roster_handler_;
+}
+
+XmppPresence*
+XmppRosterModuleImpl::outgoing_presence() {
+  return &outgoing_presence_;
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::BroadcastPresence() {
+  // Scrub the outgoing presence
+  const XmlElement* element = outgoing_presence_.raw_xml();
+
+  ASSERT(!element->HasAttr(QN_TO) &&
+         !element->HasAttr(QN_FROM) &&
+          (element->Attr(QN_TYPE) == STR_EMPTY ||
+           element->Attr(QN_TYPE) == "unavailable"));
+
+  if (!engine())
+    return XMPP_RETURN_BADSTATE;
+
+  return engine()->SendStanza(element);
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::SendDirectedPresence(const XmppPresence* presence,
+                                           const Jid& to_jid) {
+  if (!presence)
+    return XMPP_RETURN_BADARGUMENT;
+
+  if (!engine())
+    return XMPP_RETURN_BADSTATE;
+
+  XmlElement element(*(presence->raw_xml()));
+
+  if (element.Name() != QN_PRESENCE ||
+      element.HasAttr(QN_TO) ||
+      element.HasAttr(QN_FROM))
+    return XMPP_RETURN_BADARGUMENT;
+
+  if (element.HasAttr(QN_TYPE)) {
+    if (element.Attr(QN_TYPE) != STR_EMPTY &&
+        element.Attr(QN_TYPE) != "unavailable") {
+      return XMPP_RETURN_BADARGUMENT;
+    }
+  }
+
+  element.SetAttr(QN_TO, to_jid.Str());
+
+  return engine()->SendStanza(&element);
+}
+
+size_t
+XmppRosterModuleImpl::GetIncomingPresenceCount() {
+  return incoming_presence_vector_->size();
+}
+
+const XmppPresence*
+XmppRosterModuleImpl::GetIncomingPresence(size_t index) {
+  if (index >= incoming_presence_vector_->size())
+    return NULL;
+  return (*incoming_presence_vector_)[index];
+}
+
+size_t
+XmppRosterModuleImpl::GetIncomingPresenceForJidCount(const Jid& jid)
+{
+  // find the vector in the map
+  JidPresenceVectorMap::iterator pos;
+  pos = incoming_presence_map_->find(jid);
+  if (pos == incoming_presence_map_->end())
+    return 0;
+
+  ASSERT(pos->second != NULL);
+
+  return pos->second->size();
+}
+
+const XmppPresence*
+XmppRosterModuleImpl::GetIncomingPresenceForJid(const Jid& jid,
+                                                size_t index) {
+  JidPresenceVectorMap::iterator pos;
+  pos = incoming_presence_map_->find(jid);
+  if (pos == incoming_presence_map_->end())
+    return NULL;
+
+  ASSERT(pos->second != NULL);
+
+  if (index >= pos->second->size())
+    return NULL;
+
+  return (*pos->second)[index];
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::RequestRosterUpdate() {
+  if (!engine())
+    return XMPP_RETURN_BADSTATE;
+
+  XmlElement roster_get(QN_IQ);
+  roster_get.AddAttr(QN_TYPE, "get");
+  roster_get.AddAttr(QN_ID, engine()->NextId());
+  roster_get.AddElement(new XmlElement(QN_ROSTER_QUERY, true));
+  return engine()->SendIq(&roster_get, this, NULL);
+}
+
+size_t
+XmppRosterModuleImpl::GetRosterContactCount() {
+  return contacts_->size();
+}
+
+const XmppRosterContact*
+XmppRosterModuleImpl::GetRosterContact(size_t index) {
+  if (index >= contacts_->size())
+    return NULL;
+  return (*contacts_)[index];
+}
+
+class RosterPredicate {
+public:
+  explicit RosterPredicate(const Jid& jid) : jid_(jid) {
+  }
+
+  bool operator() (XmppRosterContactImpl *& contact) {
+    return contact->jid() == jid_;
+  }
+
+private:
+  Jid jid_;
+};
+
+const XmppRosterContact*
+XmppRosterModuleImpl::FindRosterContact(const Jid& jid) {
+  ContactVector::iterator pos;
+
+  pos = std::find_if(contacts_->begin(),
+                     contacts_->end(),
+                     RosterPredicate(jid));
+  if (pos == contacts_->end())
+    return NULL;
+
+  return *pos;
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::RequestRosterChange(
+  const XmppRosterContact* contact) {
+  if (!contact)
+    return XMPP_RETURN_BADARGUMENT;
+
+  Jid jid = contact->jid();
+
+  if (!jid.IsValid())
+    return XMPP_RETURN_BADARGUMENT;
+
+  if (!engine())
+    return XMPP_RETURN_BADSTATE;
+
+  const XmlElement* contact_xml = contact->raw_xml();
+  if (contact_xml->Name() != QN_ROSTER_ITEM ||
+      contact_xml->HasAttr(QN_SUBSCRIPTION) ||
+      contact_xml->HasAttr(QN_ASK))
+    return XMPP_RETURN_BADARGUMENT;
+
+  XmlElement roster_add(QN_IQ);
+  roster_add.AddAttr(QN_TYPE, "set");
+  roster_add.AddAttr(QN_ID, engine()->NextId());
+  roster_add.AddElement(new XmlElement(QN_ROSTER_QUERY, true));
+  roster_add.AddElement(new XmlElement(*contact_xml), 1);
+
+  return engine()->SendIq(&roster_add, this, NULL);
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::RequestRosterRemove(const Jid& jid) {
+  if (!jid.IsValid())
+    return XMPP_RETURN_BADARGUMENT;
+
+  if (!engine())
+    return XMPP_RETURN_BADSTATE;
+
+  XmlElement roster_add(QN_IQ);
+  roster_add.AddAttr(QN_TYPE, "set");
+  roster_add.AddAttr(QN_ID, engine()->NextId());
+  roster_add.AddElement(new XmlElement(QN_ROSTER_QUERY, true));
+  roster_add.AddAttr(QN_JID, jid.Str(), 1);
+  roster_add.AddAttr(QN_SUBSCRIPTION, "remove", 1);
+
+  return engine()->SendIq(&roster_add, this, NULL);
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::RequestSubscription(const Jid& jid) {
+  return SendSubscriptionRequest(jid, "subscribe");
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::CancelSubscription(const Jid& jid) {
+  return SendSubscriptionRequest(jid, "unsubscribe");
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::ApproveSubscriber(const Jid& jid) {
+  return SendSubscriptionRequest(jid, "subscribed");
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::CancelSubscriber(const Jid& jid) {
+  return SendSubscriptionRequest(jid, "unsubscribed");
+}
+
+void
+XmppRosterModuleImpl::IqResponse(XmppIqCookie, const XmlElement * stanza) {
+  // The only real Iq response that we expect to recieve are initial roster
+  // population
+  if (stanza->Attr(QN_TYPE) == "error")
+  {
+    if (roster_handler_)
+      roster_handler_->RosterError(this, stanza);
+
+    return;
+  }
+
+  ASSERT(stanza->Attr(QN_TYPE) == "result");
+
+  InternalRosterItems(stanza);
+}
+
+bool
+XmppRosterModuleImpl::HandleStanza(const XmlElement * stanza)
+{
+  ASSERT(engine() != NULL);
+
+  // There are two types of stanzas that we care about: presence and roster push
+  // Iqs
+  if (stanza->Name() == QN_PRESENCE) {
+    const std::string&  jid_string = stanza->Attr(QN_FROM);
+    Jid jid(jid_string);
+
+    if (!jid.IsValid())
+      return false; // if the Jid isn't valid, don't process
+
+    const std::string& type = stanza->Attr(QN_TYPE);
+    XmppSubscriptionRequestType request_type;
+    if (StringToSubscriptionRequestType(type, &request_type))
+      InternalSubscriptionRequest(jid, stanza, request_type);
+    else if (type == "unavailable" || type == STR_EMPTY)
+      InternalIncomingPresence(jid, stanza);
+    else if (type == "error")
+      InternalIncomingPresenceError(jid, stanza);
+    else
+      return false;
+
+    return true;
+  } else if (stanza->Name() == QN_IQ) {
+    const XmlElement * roster_query = stanza->FirstNamed(QN_ROSTER_QUERY);
+    if (!roster_query || stanza->Attr(QN_TYPE) != "set")
+      return false;
+
+    InternalRosterItems(stanza);
+
+    // respond to the IQ
+    XmlElement result(QN_IQ);
+    result.AddAttr(QN_TYPE, "result");
+    result.AddAttr(QN_TO, stanza->Attr(QN_FROM));
+    result.AddAttr(QN_ID, stanza->Attr(QN_ID));
+
+    engine()->SendStanza(&result);
+    return true;
+  }
+
+  return false;
+}
+
+void
+XmppRosterModuleImpl::DeleteIncomingPresence() {
+  // Clear out the vector of all presence notifications
+  {
+    PresenceVector::iterator pos;
+    for (pos = incoming_presence_vector_->begin();
+         pos < incoming_presence_vector_->end();
+         ++pos) {
+      XmppPresenceImpl * presence = *pos;
+      *pos = NULL;
+      delete presence;
+    }
+    incoming_presence_vector_->clear();
+  }
+
+  // Clear out all of the small presence vectors per Jid
+  {
+    JidPresenceVectorMap::iterator pos;
+    for (pos = incoming_presence_map_->begin();
+         pos != incoming_presence_map_->end();
+         ++pos) {
+      PresenceVector* presence_vector = pos->second;
+      pos->second = NULL;
+      delete presence_vector;
+    }
+    incoming_presence_map_->clear();
+  }
+}
+
+void
+XmppRosterModuleImpl::DeleteContacts() {
+  ContactVector::iterator pos;
+  for (pos = contacts_->begin();
+       pos < contacts_->end();
+       ++pos) {
+    XmppRosterContact* contact = *pos;
+    *pos = NULL;
+    delete contact;
+  }
+  contacts_->clear();
+}
+
+XmppReturnStatus
+XmppRosterModuleImpl::SendSubscriptionRequest(const Jid& jid,
+                                              const std::string& type) {
+  if (!jid.IsValid())
+    return XMPP_RETURN_BADARGUMENT;
+
+  if (!engine())
+    return XMPP_RETURN_BADSTATE;
+
+  XmlElement presence_request(QN_PRESENCE);
+  presence_request.AddAttr(QN_TO, jid.Str());
+  presence_request.AddAttr(QN_TYPE, type);
+
+  return engine()->SendStanza(&presence_request);
+}
+
+
+void
+XmppRosterModuleImpl::InternalSubscriptionRequest(const Jid& jid,
+                                                  const XmlElement* stanza,
+                                                  XmppSubscriptionRequestType
+                                                    request_type) {
+  if (roster_handler_)
+    roster_handler_->SubscriptionRequest(this, jid, request_type, stanza);
+}
+
+class PresencePredicate {
+public:
+  explicit PresencePredicate(const Jid& jid) : jid_(jid) {
+  }
+
+  bool operator() (XmppPresenceImpl *& contact) {
+    return contact->jid() == jid_;
+  }
+
+private:
+  Jid jid_;
+};
+
+void
+XmppRosterModuleImpl::InternalIncomingPresence(const Jid& jid,
+                                               const XmlElement* stanza) {
+  bool added = false;
+  Jid bare_jid = jid.BareJid();
+
+  // First add the presence to the map
+  JidPresenceVectorMap::iterator pos;
+  pos = incoming_presence_map_->find(jid.BareJid());
+  if (pos == incoming_presence_map_->end()) {
+    // Insert a new entry into the map.  Get the position of this new entry
+    pos = (incoming_presence_map_->insert(
+            std::make_pair(bare_jid, new PresenceVector()))).first;
+  }
+
+  PresenceVector * presence_vector = pos->second;
+  ASSERT(presence_vector != NULL);
+
+  // Try to find this jid in the bare jid bucket
+  PresenceVector::iterator presence_pos;
+  XmppPresenceImpl* presence;
+  presence_pos = std::find_if(presence_vector->begin(),
+                              presence_vector->end(),
+                              PresencePredicate(jid));
+
+  // Update/add it to the bucket
+  if (presence_pos == presence_vector->end()) {
+    presence = new XmppPresenceImpl();
+    if (XMPP_RETURN_OK == presence->set_raw_xml(stanza)) {
+      added = true;
+      presence_vector->push_back(presence);
+    } else {
+      delete presence;
+      presence = NULL;
+    }
+  } else {
+    presence = *presence_pos;
+    presence->set_raw_xml(stanza);
+  }
+
+  // now add to the comprehensive vector
+  if (added)
+    incoming_presence_vector_->push_back(presence);
+
+  // Call back to the user with the changed presence information
+  if (roster_handler_)
+    roster_handler_->IncomingPresenceChanged(this, presence);
+}
+
+
+void
+XmppRosterModuleImpl::InternalIncomingPresenceError(const Jid& jid,
+                                                    const XmlElement* stanza) {
+  if (roster_handler_)
+    roster_handler_->SubscriptionError(this, jid, stanza);
+}
+
+void
+XmppRosterModuleImpl::InternalRosterItems(const XmlElement* stanza) {
+  const XmlElement* result_data = stanza->FirstNamed(QN_ROSTER_QUERY);
+  if (!result_data)
+    return; // unknown stuff in result!
+
+  bool all_new = contacts_->empty();
+
+  for (const XmlElement* roster_item = result_data->FirstNamed(QN_ROSTER_ITEM);
+       roster_item;
+       roster_item = roster_item->NextNamed(QN_ROSTER_ITEM))
+  {
+    const std::string& jid_string = roster_item->Attr(QN_JID);
+    Jid jid(jid_string);
+    if (!jid.IsValid())
+      continue;
+
+    // This algorithm is N^2 on the number of incoming contacts after the
+    // initial load. There is no way to do this faster without allowing
+    // duplicates, introducing more data structures or write a custom data
+    // structure.  We'll see if this becomes a perf problem and fix it if it
+    // does.
+    ContactVector::iterator pos = contacts_->end();
+
+    if (!all_new) {
+      pos = std::find_if(contacts_->begin(),
+                         contacts_->end(),
+                         RosterPredicate(jid));
+    }
+
+    if (pos != contacts_->end()) { // Update/remove a current contact
+      if (roster_item->Attr(QN_SUBSCRIPTION) == "remove") {
+        XmppRosterContact* contact = *pos;
+        contacts_->erase(pos);
+        if (roster_handler_)
+          roster_handler_->ContactRemoved(this, contact,
+            std::distance(contacts_->begin(), pos));
+        delete contact;
+      } else {
+        XmppRosterContact* old_contact = *pos;
+        *pos = new XmppRosterContactImpl();
+        (*pos)->SetXmlFromWire(roster_item);
+        if (roster_handler_)
+          roster_handler_->ContactChanged(this, old_contact,
+            std::distance(contacts_->begin(), pos));
+        delete old_contact;
+      }
+    } else { // Add a new contact
+      XmppRosterContactImpl* contact = new XmppRosterContactImpl();
+      contact->SetXmlFromWire(roster_item);
+      contacts_->push_back(contact);
+      if (roster_handler_ && !all_new)
+        roster_handler_->ContactsAdded(this, contacts_->size() - 1, 1);
+    }
+  }
+
+  // Send a consolidated update if all contacts are new
+  if (roster_handler_ && all_new)
+    roster_handler_->ContactsAdded(this, 0, contacts_->size());
+}
+
+}
diff --git a/talk/xmpp/rostermoduleimpl.h b/talk/xmpp/rostermoduleimpl.h
new file mode 100644
index 0000000..48ad5e5
--- /dev/null
+++ b/talk/xmpp/rostermoduleimpl.h
@@ -0,0 +1,293 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, 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.
+ */
+
+#ifndef _rostermoduleimpl_h_
+#define _rostermoduleimpl_h_
+
+#include "talk/xmpp/moduleimpl.h"
+#include "talk/xmpp/rostermodule.h"
+
+namespace buzz {
+
+//! Presence Information
+//! This class stores both presence information for outgoing presence and is
+//! returned by methods in XmppRosterModule to represent recieved incoming
+//! presence information.  When this class is writeable (non-const) then each
+//! update to any property will set the inner xml.  Setting the raw_xml will
+//! rederive all of the other properties.
+class XmppPresenceImpl : public XmppPresence {
+public:
+  virtual ~XmppPresenceImpl() {}
+
+  //! The from Jid of for the presence information.
+  //! Typically this will be a full Jid with resource specified.  For outgoing
+  //! presence this should remain JID_NULL and will be scrubbed from the
+  //! stanza when being sent.
+  virtual const Jid jid() const;
+
+  //! Is the contact available?
+  virtual XmppPresenceAvailable available() const;
+
+  //! Sets if the user is available or not
+  virtual XmppReturnStatus set_available(XmppPresenceAvailable available);
+
+  //! The show value of the presence info
+  virtual XmppPresenceShow presence_show() const;
+
+  //! Set the presence show value
+  virtual XmppReturnStatus set_presence_show(XmppPresenceShow show);
+
+  //! The Priority of the presence info
+  virtual int priority() const;
+
+  //! Set the priority of the presence
+  virtual XmppReturnStatus set_priority(int priority);
+
+  //! The plain text status of the presence info.
+  //! If there are multiple status because of language, this will either be a
+  //! status that is not tagged for language or the first available
+  virtual const std::string& status() const;
+
+  //! Sets the status for the presence info.
+  //! If there is more than one status present already then this will remove
+  //! them all and replace it with one status element we no specified language
+  virtual XmppReturnStatus set_status(const std::string& status);
+
+  //! The raw xml of the presence update
+  virtual const XmlElement* raw_xml() const;
+
+  //! Sets the raw presence stanza for the presence update
+  //! This will cause all other data items in this structure to be rederived
+  virtual XmppReturnStatus set_raw_xml(const XmlElement * xml);
+
+private:
+  XmppPresenceImpl();
+
+  friend class XmppPresence;
+  friend class XmppRosterModuleImpl;
+
+  void CreateRawXmlSkeleton();
+
+  // Store everything in the XML element. If this becomes a perf issue we can
+  // cache the data.
+  talk_base::scoped_ptr<XmlElement> raw_xml_;
+};
+
+//! A contact as given by the server
+class XmppRosterContactImpl : public XmppRosterContact {
+public:
+  virtual ~XmppRosterContactImpl() {}
+
+  //! The jid for the contact.
+  //! Typically this will be a bare Jid.
+  virtual const Jid jid() const;
+
+  //! Sets the jid for the roster contact update
+  virtual XmppReturnStatus set_jid(const Jid& jid);
+
+  //! The name (nickname) stored for this contact
+  virtual const std::string& name() const;
+
+  //! Sets the name
+  virtual XmppReturnStatus set_name(const std::string& name);
+
+  //! The Presence subscription state stored on the server for this contact
+  //! This is never settable and will be ignored when generating a roster
+  //! add/update request
+  virtual XmppSubscriptionState subscription_state() const;
+
+  //! The number of Groups applied to this contact
+  virtual size_t GetGroupCount() const;
+
+  //! Gets a Group applied to the contact based on index.
+  virtual const std::string& GetGroup(size_t index) const;
+
+  //! Adds a group to this contact.
+  //! This will return a no error if the group is already present.
+  virtual XmppReturnStatus AddGroup(const std::string& group);
+
+  //! Removes a group from the contact.
+  //! This will return no error if the group isn't there
+  virtual XmppReturnStatus RemoveGroup(const std::string& group);
+
+  //! The raw xml for this roster contact
+  virtual const XmlElement* raw_xml() const;
+
+  //! Sets the raw presence stanza for the presence update
+  //! This will cause all other data items in this structure to be rederived
+  virtual XmppReturnStatus set_raw_xml(const XmlElement * xml);
+
+private:
+  XmppRosterContactImpl();
+
+  void CreateRawXmlSkeleton();
+  void SetXmlFromWire(const XmlElement * xml);
+  void ResetGroupCache();
+
+  bool FindGroup(const std::string& group,
+                 XmlElement** element,
+                 XmlChild** child_before);
+
+
+  friend class XmppRosterContact;
+  friend class XmppRosterModuleImpl;
+
+  int group_count_;
+  int group_index_returned_;
+  XmlElement * group_returned_;
+  talk_base::scoped_ptr<XmlElement> raw_xml_;
+};
+
+//! An XmppModule for handle roster and presence functionality
+class XmppRosterModuleImpl : public XmppModuleImpl,
+  public XmppRosterModule, public XmppIqHandler {
+public:
+  virtual ~XmppRosterModuleImpl();
+
+  IMPLEMENT_XMPPMODULE
+
+  //! Sets the roster handler (callbacks) for the module
+  virtual XmppReturnStatus set_roster_handler(XmppRosterHandler * handler);
+
+  //! Gets the roster handler for the module
+  virtual XmppRosterHandler* roster_handler();
+
+  // USER PRESENCE STATE -------------------------------------------------------
+
+  //! Gets the aggregate outgoing presence
+  //! This object is non-const and be edited directly.  No update is sent
+  //! to the server until a Broadcast is sent
+  virtual XmppPresence* outgoing_presence();
+
+  //! Broadcasts that the user is available.
+  //! Nothing with respect to presence is sent until this is called.
+  virtual XmppReturnStatus BroadcastPresence();
+
+  //! Sends a directed presence to a Jid
+  //! Note that the client doesn't store where directed presence notifications
+  //! have been sent.  The server can keep the appropriate state
+  virtual XmppReturnStatus SendDirectedPresence(const XmppPresence* presence,
+                                                const Jid& to_jid);
+
+  // INCOMING PRESENCE STATUS --------------------------------------------------
+
+  //! Returns the number of incoming presence data recorded
+  virtual size_t GetIncomingPresenceCount();
+
+  //! Returns an incoming presence datum based on index
+  virtual const XmppPresence* GetIncomingPresence(size_t index);
+
+  //! Gets the number of presence data for a bare Jid
+  //! There may be a datum per resource
+  virtual size_t GetIncomingPresenceForJidCount(const Jid& jid);
+
+  //! Returns a single presence data for a Jid based on index
+  virtual const XmppPresence* GetIncomingPresenceForJid(const Jid& jid,
+                                                        size_t index);
+
+  // ROSTER MANAGEMENT ---------------------------------------------------------
+
+  //! Requests an update of the roster from the server
+  //! This must be called to initialize the client side cache of the roster
+  //! After this is sent the server should keep this module apprised of any
+  //! changes.
+  virtual XmppReturnStatus RequestRosterUpdate();
+
+  //! Returns the number of contacts in the roster
+  virtual size_t GetRosterContactCount();
+
+  //! Returns a contact by index
+  virtual const XmppRosterContact* GetRosterContact(size_t index);
+
+  //! Finds a contact by Jid
+  virtual const XmppRosterContact* FindRosterContact(const Jid& jid);
+
+  //! Send a request to the server to add a contact
+  //! Note that the contact won't show up in the roster until the server can
+  //! respond.  This happens async when the socket is being serviced
+  virtual XmppReturnStatus RequestRosterChange(
+    const XmppRosterContact* contact);
+
+  //! Request that the server remove a contact
+  //! The jabber protocol specifies that the server should also cancel any
+  //! subscriptions when this is done.  Like adding, this contact won't be
+  //! removed until the server responds.
+  virtual XmppReturnStatus RequestRosterRemove(const Jid& jid);
+
+  // SUBSCRIPTION MANAGEMENT ---------------------------------------------------
+
+  //! Request a subscription to presence notifications form a Jid
+  virtual XmppReturnStatus RequestSubscription(const Jid& jid);
+
+  //! Cancel a subscription to presence notifications from a Jid
+  virtual XmppReturnStatus CancelSubscription(const Jid& jid);
+
+  //! Approve a request to deliver presence notifications to a jid
+  virtual XmppReturnStatus ApproveSubscriber(const Jid& jid);
+
+  //! Deny or cancel presence notification deliver to a jid
+  virtual XmppReturnStatus CancelSubscriber(const Jid& jid);
+
+  // XmppIqHandler IMPLEMENTATION ----------------------------------------------
+  virtual void IqResponse(XmppIqCookie cookie, const XmlElement * stanza);
+
+protected:
+  // XmppModuleImpl OVERRIDES --------------------------------------------------
+  virtual bool HandleStanza(const XmlElement *);
+
+  // PRIVATE DATA --------------------------------------------------------------
+private:
+  friend class XmppRosterModule;
+  XmppRosterModuleImpl();
+
+  // Helper functions
+  void DeleteIncomingPresence();
+  void DeleteContacts();
+  XmppReturnStatus SendSubscriptionRequest(const Jid& jid,
+                                           const std::string& type);
+  void InternalSubscriptionRequest(const Jid& jid, const XmlElement* stanza,
+                                   XmppSubscriptionRequestType request_type);
+  void InternalIncomingPresence(const Jid& jid, const XmlElement* stanza);
+  void InternalIncomingPresenceError(const Jid& jid, const XmlElement* stanza);
+  void InternalRosterItems(const XmlElement* stanza);
+
+  // Member data
+  XmppPresenceImpl outgoing_presence_;
+  XmppRosterHandler* roster_handler_;
+
+  typedef std::vector<XmppPresenceImpl*> PresenceVector;
+  typedef std::map<Jid, PresenceVector*> JidPresenceVectorMap;
+  talk_base::scoped_ptr<JidPresenceVectorMap> incoming_presence_map_;
+  talk_base::scoped_ptr<PresenceVector> incoming_presence_vector_;
+
+  typedef std::vector<XmppRosterContactImpl*> ContactVector;
+  talk_base::scoped_ptr<ContactVector> contacts_;
+};
+
+}
+
+#endif