Add support for WebSocket.addUtf8Text().

Also update WebSocketImpl to the latest SDK version.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86a1110..110e2f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.1.0
+
+* Add `WebSocketSink.addUtf8Text()`. This matches `dart:io`'s
+  `WebSocket.addUtf8Text()`.
+
 ## 1.0.4
 
 * Support `crypto` 2.0.0.
diff --git a/lib/html.dart b/lib/html.dart
index 07a17ed..af390d2 100644
--- a/lib/html.dart
+++ b/lib/html.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:html';
 import 'dart:typed_data';
 
@@ -129,6 +130,8 @@
       : _channel = channel,
         super(channel._controller.foreign.sink);
 
+  void addUtf8Text(List<int> bytes) => super.add(UTF8.decode(bytes));
+
   Future close([int closeCode, String closeReason]) {
     _channel._localCloseCode = closeCode;
     _channel._localCloseReason = closeReason;
diff --git a/lib/io.dart b/lib/io.dart
index 8bf087b..1ea1aab 100644
--- a/lib/io.dart
+++ b/lib/io.dart
@@ -88,6 +88,8 @@
       : _webSocket = webSocket,
         super(webSocket);
 
+  void addUtf8Text(List<int> bytes) => _webSocket.addUtf8Text(bytes);
+
   Future close([int closeCode, String closeReason]) =>
       _webSocket.close(closeCode, closeReason);
 }
diff --git a/lib/src/channel.dart b/lib/src/channel.dart
index 6aff42c..a5d2956 100644
--- a/lib/src/channel.dart
+++ b/lib/src/channel.dart
@@ -98,8 +98,8 @@
 
 /// The sink exposed by a [WebSocketChannel].
 ///
-/// This is like a normal [StreamSink], except that it supports extra arguments
-/// to [close].
+/// This is like a normal [StreamSink], except that it supports [addUtf8Text]
+/// and extra arguments to [close].
 class WebSocketSink extends DelegatingStreamSink {
   final WebSocketImpl _webSocket;
 
@@ -107,6 +107,16 @@
       : _webSocket = webSocket,
         super(webSocket);
 
+  /// Sends a text message with text represented by [bytes].
+  ///
+  /// The [bytes] should be valid UTF-8 encoded Unicode characters. If they are
+  /// not, the receiving end will close the connection.
+  ///
+  /// On some implementations (such as [IOWebSocketChannel]), this avoids the
+  /// overhead of encoding [bytes] as a UTF-16 Dart string and then back to
+  /// UTF-8 to send across the socket.
+  void addUtf8Text(List<int> bytes) => _webSocket.addUtf8Text(bytes);
+
   /// Closes the web socket connection.
   ///
   /// [closeCode] and [closeReason] are the [close code][] and [reason][] sent
diff --git a/lib/src/copy/bytes_builder.dart b/lib/src/copy/bytes_builder.dart
index 573eed5..3d200db 100644
--- a/lib/src/copy/bytes_builder.dart
+++ b/lib/src/copy/bytes_builder.dart
@@ -9,7 +9,7 @@
 // Because it's copied directly, there are no modifications from the original.
 //
 // This is up-to-date as of sdk revision
-// e41fb4cafd6052157dbc1490d437045240f4773f.
+// b40c20bfbccd0e4d98d194b9d362d04a9f7c91f3.
 
 import 'dart:math';
 import 'dart:typed_data';
@@ -55,7 +55,7 @@
   /**
    * Returns the contents of `this` and clears `this`.
    *
-   * The list returned is a view of the the internal buffer, limited to the
+   * The list returned is a view of the internal buffer, limited to the
    * [length].
    */
   List<int> takeBytes();
@@ -163,14 +163,17 @@
 
 class _BytesBuilder implements BytesBuilder {
   int _length = 0;
-  final _chunks = <List<int>>[];
+  final List<Uint8List> _chunks = [];
 
   void add(List<int> bytes) {
-    if (bytes is! Uint8List) {
-      bytes = new Uint8List.fromList(bytes);
+    Uint8List typedBytes;
+    if (bytes is Uint8List) {
+      typedBytes = bytes;
+    } else {
+      typedBytes = new Uint8List.fromList(bytes);
     }
-    _chunks.add(bytes);
-    _length += bytes.length;
+    _chunks.add(typedBytes);
+    _length += typedBytes.length;
   }
 
   void addByte(int byte) { add([byte]); }
diff --git a/lib/src/copy/io_sink.dart b/lib/src/copy/io_sink.dart
index 34abcad..86bab15 100644
--- a/lib/src/copy/io_sink.dart
+++ b/lib/src/copy/io_sink.dart
@@ -9,7 +9,7 @@
 // desired public API and to remove "dart:io" dependencies have been made.
 //
 // This is up-to-date as of sdk revision
-// e41fb4cafd6052157dbc1490d437045240f4773f.
+// b40c20bfbccd0e4d98d194b9d362d04a9f7c91f3.
 
 import 'dart:async';
 
diff --git a/lib/src/copy/web_socket.dart b/lib/src/copy/web_socket.dart
index c77dc2f..01ba332 100644
--- a/lib/src/copy/web_socket.dart
+++ b/lib/src/copy/web_socket.dart
@@ -9,7 +9,7 @@
 // desired public API and to remove "dart:io" dependencies have been made.
 //
 // This is up-to-date as of sdk revision
-// e41fb4cafd6052157dbc1490d437045240f4773f.
+// b40c20bfbccd0e4d98d194b9d362d04a9f7c91f3.
 
 /**
  * Web socket status codes used when closing a web socket connection.
diff --git a/lib/src/copy/web_socket_impl.dart b/lib/src/copy/web_socket_impl.dart
index 38db9e3..9aa83d1 100644
--- a/lib/src/copy/web_socket_impl.dart
+++ b/lib/src/copy/web_socket_impl.dart
@@ -10,7 +10,7 @@
 // desired public API and to remove "dart:io" dependencies have been made.
 //
 // This is up-to-date as of sdk revision
-// e41fb4cafd6052157dbc1490d437045240f4773f.
+// b40c20bfbccd0e4d98d194b9d362d04a9f7c91f3.
 
 import 'dart:async';
 import 'dart:convert';
@@ -57,21 +57,14 @@
   static const int RESERVED_F = 15;
 }
 
-/**
- *  Stores the header and integer value derived from negotiation of
- *  client_max_window_bits and server_max_window_bits. headerValue will be
- *  set in the Websocket response headers.
- */
-class _CompressionMaxWindowBits {
-  String headerValue;
-  int maxWindowBits;
-  _CompressionMaxWindowBits([this.headerValue, this.maxWindowBits]);
-  String toString() => headerValue;
+class _EncodedString {
+  final List<int> bytes;
+  _EncodedString(this.bytes);
 }
 
 /**
  * The web socket protocol transformer handles the protocol byte stream
- * which is supplied through the [:handleData:]. As the protocol is processed,
+ * which is supplied through the `handleData`. As the protocol is processed,
  * it'll output frame data as either a List<int> or String.
  *
  * Important information about usage: Be sure you use cancelOnError, so the
@@ -80,7 +73,8 @@
  */
 // TODO(ajohnsen): make this transformer reusable?
 class _WebSocketProtocolTransformer
-    implements StreamTransformer<List<int>, dynamic>, EventSink<List<int>> {
+    implements EventSink<List<int>>, StreamTransformer<
+        List<int>, dynamic/*List<int>|_WebSocketPing|_WebSocketPong>*/> {
   static const int START = 0;
   static const int LEN_FIRST = 1;
   static const int LEN_REST = 2;
@@ -108,7 +102,7 @@
   int closeCode = WebSocketStatus.NO_STATUS_RECEIVED;
   String closeReason = "";
 
-  EventSink _eventSink;
+  EventSink<dynamic/*List<int>|_WebSocketPing|_WebSocketPong>*/> _eventSink;
 
   final bool _serverSide;
   final List _maskingBytes = new List(4);
@@ -116,7 +110,8 @@
 
   _WebSocketProtocolTransformer([this._serverSide = false]);
 
-  Stream bind(Stream stream) {
+  Stream<dynamic/*List<int>|_WebSocketPing|_WebSocketPong>*/> bind(
+      Stream<List<int>> stream) {
     return new Stream.eventTransformed(stream, (EventSink eventSink) {
       if (_eventSink != null) {
         throw new StateError("WebSocket transformer already used.");
@@ -422,7 +417,8 @@
   _WebSocketOutgoingTransformer(this.webSocket);
 
   Stream<List<int>> bind(Stream stream) {
-    return new Stream.eventTransformed(stream, (eventSink) {
+    return new Stream<List<int>>.eventTransformed(
+        stream, (EventSink<List<int>> eventSink) {
       if (_eventSink != null) {
         throw new StateError("WebSocket transformer already used");
       }
@@ -446,13 +442,14 @@
       if (message is String) {
         opcode = _WebSocketOpcode.TEXT;
         data = UTF8.encode(message);
+      } else if (message is List<int>) {
+        opcode = _WebSocketOpcode.BINARY;
+        data = message;
+      } else if (message is _EncodedString) {
+        opcode = _WebSocketOpcode.TEXT;
+        data = message.bytes;
       } else {
-        if (message is List<int>) {
-          data = message;
-          opcode = _WebSocketOpcode.BINARY;
-        } else {
-          throw new ArgumentError(message);
-        }
+        throw new ArgumentError(message);
       }
     } else {
       opcode = _WebSocketOpcode.TEXT;
@@ -484,9 +481,8 @@
           opcode,
           data,
           webSocket._serverSide,
-          false).forEach((e) {
-        _eventSink.add(e);
-      });
+          false)
+        .forEach((e) { _eventSink.add(e); });
 
   static Iterable<List<int>> createFrame(
       int opcode, List<int> data, bool serverSide, bool compressed) {
@@ -807,6 +803,12 @@
   String get closeReason => _closeReason;
 
   void add(data) { _sink.add(data); }
+  void addUtf8Text(List<int> bytes) {
+    if (bytes is! List<int>) {
+      throw new ArgumentError.value(bytes, "bytes", "Is not a list of bytes");
+    }
+    _sink.add(new _EncodedString(bytes));
+  }
   void addError(error, [StackTrace stackTrace]) {
     _sink.addError(error, stackTrace);
   }
diff --git a/lib/src/sink_completer.dart b/lib/src/sink_completer.dart
index d932fd7..5d1187a 100644
--- a/lib/src/sink_completer.dart
+++ b/lib/src/sink_completer.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 
 import 'channel.dart';
 
@@ -90,6 +91,17 @@
     }
   }
 
+  void addUtf8Text(List<int> bytes) {
+    if (_canSendDirectly) {
+      _destinationSink.addUtf8Text(bytes);
+    } else {
+      _ensureController();
+      // Ideally we wouldn't ever have to decode these bytes, but it's the
+      // cleanest way to get [setDestinationSink] to work.
+      _controller.add(UTF8.decode(bytes));
+    }
+  }
+
   void addError(error, [StackTrace stackTrace]) {
     if (_canSendDirectly) {
       _destinationSink.addError(error, stackTrace);
diff --git a/pubspec.yaml b/pubspec.yaml
index 3a95d49..ccb7d26 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,11 +1,11 @@
 name: web_socket_channel
-version: 1.0.4
+version: 1.1.0
 description: StreamChannel wrappers for WebSockets.
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/web_socket_channel
 
 environment:
-  sdk: '>=1.13.0 <2.0.0'
+  sdk: '>=1.20.0 <2.0.0'
 
 dependencies:
   async: '^1.3.0'
diff --git a/test/html_test.dart b/test/html_test.dart
index 9954eb6..690327c 100644
--- a/test/html_test.dart
+++ b/test/html_test.dart
@@ -10,6 +10,7 @@
     "html_test_server.dart.")
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:html';
 import 'dart:typed_data';
 
@@ -52,6 +53,15 @@
     expect(await queue.next, equals("foo"));
   });
 
+  test("sends raw UTF-8 bytes", () async {
+    var webSocket = new WebSocket("ws://localhost:1234");
+    channel = new HtmlWebSocketChannel(webSocket);
+
+    var queue = new StreamQueue(channel.stream);
+    channel.sink.addUtf8Text(UTF8.encode("ṗĭñḡ"));
+    expect(await queue.next, equals("ṗĭñḡ"));
+  });
+
   test(".connect defaults to binary lists", () async {
     channel = new HtmlWebSocketChannel.connect("ws://localhost:1234");
 
diff --git a/test/io_test.dart b/test/io_test.dart
index fcf8c7c..53a63eb 100644
--- a/test/io_test.dart
+++ b/test/io_test.dart
@@ -5,6 +5,7 @@
 @TestOn('vm')
 
 import 'dart:io';
+import 'dart:convert';
 
 import 'package:test/test.dart';
 
@@ -49,6 +50,21 @@
     }));
   });
 
+  test("sends raw UTF-8 bytes", () async {
+    server = await HttpServer.bind("localhost", 0);
+    server.transform(new WebSocketTransformer()).listen((webSocket) {
+      var channel = new IOWebSocketChannel(webSocket);
+      channel.stream.listen((request) {
+        expect(request, equals("ṗĭñḡ"));
+        channel.close();
+      });
+    });
+
+    var channel = new IOWebSocketChannel.connect(
+        "ws://localhost:${server.port}");
+    channel.sink.addUtf8Text(UTF8.encode("ṗĭñḡ"));
+  });
+
   test(".connect communicates immediately", () async {
     server = await HttpServer.bind("localhost", 0);
     server.transform(new WebSocketTransformer()).listen((webSocket) {