Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(287)

Side by Side Diff: components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java

Issue 1412243012: Initial implementation of CronetBidirectionalStream. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix javadoc link. Created 4 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package org.chromium.net; 5 package org.chromium.net;
6 6
7 import android.os.ConditionVariable;
7 import android.test.suitebuilder.annotation.SmallTest; 8 import android.test.suitebuilder.annotation.SmallTest;
8 9
9 import org.chromium.base.test.util.Feature; 10 import org.chromium.base.test.util.Feature;
11 import org.chromium.net.CronetTestBase.OnlyRunNativeCronet;
12 import org.chromium.net.TestBidirectionalStreamCallback.FailureType;
13 import org.chromium.net.TestBidirectionalStreamCallback.ResponseStep;
10 14
11 import java.nio.ByteBuffer; 15 import java.nio.ByteBuffer;
12 import java.util.concurrent.Executor; 16 import java.util.AbstractMap;
13 import java.util.concurrent.ExecutorService; 17 import java.util.ArrayList;
14 import java.util.concurrent.Executors; 18 import java.util.Arrays;
15 import java.util.concurrent.ThreadFactory; 19 import java.util.List;
20 import java.util.Map;
21 import java.util.regex.Matcher;
22 import java.util.regex.Pattern;
16 23
17 /** 24 /**
18 * Test functionality of BidirectionalStream interface. 25 * Test functionality of BidirectionalStream interface.
19 */ 26 */
20 public class BidirectionalStreamTest extends CronetTestBase { 27 public class BidirectionalStreamTest extends CronetTestBase {
21 private CronetTestFramework mTestFramework; 28 private CronetTestFramework mTestFramework;
22 29
23 @Override 30 @Override
24 protected void setUp() throws Exception { 31 protected void setUp() throws Exception {
25 super.setUp(); 32 super.setUp();
26 mTestFramework = startCronetTestFramework(); 33 // Load library first to create MockCertVerifier.
27 assertTrue(NativeTestServer.startNativeTestServer(getContext())); 34 System.loadLibrary("cronet_tests");
28 // Add url interceptors after native application context is initialized. 35 CronetEngine.Builder builder = new CronetEngine.Builder(getContext());
29 MockUrlRequestJobFactory.setUp(); 36 builder.setMockCertVerifierForTesting(QuicTestServer.createMockCertVerif ier());
37
38 mTestFramework = startCronetTestFrameworkWithUrlAndCronetEngineBuilder(n ull, builder);
39 assertTrue(Http2TestServer.startHttp2TestServer(
40 getContext(), QuicTestServer.getServerCert(), QuicTestServer.get ServerCertKey()));
30 } 41 }
31 42
32 @Override 43 @Override
33 protected void tearDown() throws Exception { 44 protected void tearDown() throws Exception {
34 NativeTestServer.shutdownNativeTestServer(); 45 assertTrue(Http2TestServer.shutdownHttp2TestServer());
35 mTestFramework.mCronetEngine.shutdown(); 46 if (mTestFramework.mCronetEngine != null) {
47 mTestFramework.mCronetEngine.shutdown();
48 }
36 super.tearDown(); 49 super.tearDown();
37 } 50 }
38 51
39 private class TestBidirectionalStreamCallback extends BidirectionalStream.Ca llback { 52 private static void checkResponseInfo(UrlResponseInfo responseInfo, String e xpectedUrl,
40 // Executor Service for Cronet callbacks. 53 int expectedHttpStatusCode, String expectedHttpStatusText) {
41 private final ExecutorService mExecutorService = 54 assertEquals(expectedUrl, responseInfo.getUrl());
42 Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); 55 assertEquals(
43 private Thread mExecutorThread; 56 expectedUrl, responseInfo.getUrlChain().get(responseInfo.getUrlC hain().size() - 1));
57 assertEquals(expectedHttpStatusCode, responseInfo.getHttpStatusCode());
58 assertEquals(expectedHttpStatusText, responseInfo.getHttpStatusText());
59 assertFalse(responseInfo.wasCached());
60 assertTrue(responseInfo.toString().length() > 0);
61 }
44 62
45 private class ExecutorThreadFactory implements ThreadFactory { 63 private static String createLongString(String base, int repetition) {
46 public Thread newThread(Runnable r) { 64 StringBuilder builder = new StringBuilder(base.length() * repetition);
47 mExecutorThread = new Thread(r); 65 for (int i = 0; i < repetition; ++i) {
48 return mExecutorThread; 66 builder.append(i);
49 } 67 builder.append(base);
50 } 68 }
69 return builder.toString();
70 }
51 71
52 @Override 72 private static UrlResponseInfo createUrlResponseInfo(
53 public void onRequestHeadersSent(BidirectionalStream stream) {} 73 String[] urls, String message, int statusCode, int receivedBytes, St ring... headers) {
54 74 ArrayList<Map.Entry<String, String>> headersList = new ArrayList<>();
55 @Override 75 for (int i = 0; i < headers.length; i += 2) {
56 public void onResponseHeadersReceived(BidirectionalStream stream, UrlRes ponseInfo info) {} 76 headersList.add(new AbstractMap.SimpleImmutableEntry<String, String> (
57 77 headers[i], headers[i + 1]));
58 @Override
59 public void onReadCompleted(
60 BidirectionalStream stream, UrlResponseInfo info, ByteBuffer buf fer) {}
61
62 @Override
63 public void onWriteCompleted(
64 BidirectionalStream stream, UrlResponseInfo info, ByteBuffer buf fer) {}
65
66 @Override
67 public void onResponseTrailersReceived(BidirectionalStream stream, UrlRe sponseInfo info,
68 UrlResponseInfo.HeaderBlock trailers) {}
69
70 @Override
71 public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info ) {}
72
73 @Override
74 public void onFailed(
75 BidirectionalStream stream, UrlResponseInfo info, CronetExceptio n error) {}
76
77 @Override
78 public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) {}
79
80 Executor getExecutor() {
81 return mExecutorService;
82 } 78 }
79 UrlResponseInfo urlResponseInfo = new UrlResponseInfo(
80 Arrays.asList(urls), statusCode, message, headersList, false, "h 2", null);
81 urlResponseInfo.setReceivedBytesCount(receivedBytes);
82 return urlResponseInfo;
83 } 83 }
84 84
85 @SmallTest 85 @SmallTest
86 @Feature({"Cronet"}) 86 @Feature({"Cronet"})
87 public void testBuilderChecks() throws Exception { 87 public void testBuilderChecks() throws Exception {
88 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback(); 88 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
89 try { 89 try {
90 new BidirectionalStream.Builder( 90 new BidirectionalStream.Builder(
91 null, callback, callback.getExecutor(), mTestFramework.mCron etEngine); 91 null, callback, callback.getExecutor(), mTestFramework.mCron etEngine);
92 fail("URL not null-checked"); 92 fail("URL not null-checked");
93 } catch (NullPointerException e) { 93 } catch (NullPointerException e) {
94 assertEquals("URL is required.", e.getMessage()); 94 assertEquals("URL is required.", e.getMessage());
95 } 95 }
96 try { 96 try {
97 new BidirectionalStream.Builder(NativeTestServer.getRedirectURL(), n ull, 97 new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), null ,
98 callback.getExecutor(), mTestFramework.mCronetEngine); 98 callback.getExecutor(), mTestFramework.mCronetEngine);
99 fail("Callback not null-checked"); 99 fail("Callback not null-checked");
100 } catch (NullPointerException e) { 100 } catch (NullPointerException e) {
101 assertEquals("Callback is required.", e.getMessage()); 101 assertEquals("Callback is required.", e.getMessage());
102 } 102 }
103 try { 103 try {
104 new BidirectionalStream.Builder(NativeTestServer.getRedirectURL(), c allback, null, 104 new BidirectionalStream.Builder(
105 mTestFramework.mCronetEngine); 105 Http2TestServer.getServerUrl(), callback, null, mTestFramewo rk.mCronetEngine);
106 fail("Executor not null-checked"); 106 fail("Executor not null-checked");
107 } catch (NullPointerException e) { 107 } catch (NullPointerException e) {
108 assertEquals("Executor is required.", e.getMessage()); 108 assertEquals("Executor is required.", e.getMessage());
109 } 109 }
110 try { 110 try {
111 new BidirectionalStream.Builder( 111 new BidirectionalStream.Builder(
112 NativeTestServer.getRedirectURL(), callback, callback.getExe cutor(), null); 112 Http2TestServer.getServerUrl(), callback, callback.getExecut or(), null);
113 fail("CronetEngine not null-checked"); 113 fail("CronetEngine not null-checked");
114 } catch (NullPointerException e) { 114 } catch (NullPointerException e) {
115 assertEquals("CronetEngine is required.", e.getMessage()); 115 assertEquals("CronetEngine is required.", e.getMessage());
116 } 116 }
117 // Verify successful creation doesn't throw. 117 // Verify successful creation doesn't throw.
118 new BidirectionalStream.Builder(NativeTestServer.getRedirectURL(), callb ack, 118 BidirectionalStream.Builder builder =
119 callback.getExecutor(), mTestFramework.mCronetEngine); 119 new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
120 callback.getExecutor(), mTestFramework.mCronetEngine);
121 try {
122 builder.addHeader(null, "value");
123 fail("Header name is not null-checked");
124 } catch (NullPointerException e) {
125 assertEquals("Invalid header name.", e.getMessage());
126 }
127 try {
128 builder.addHeader("name", null);
129 fail("Header value is not null-checked");
130 } catch (NullPointerException e) {
131 assertEquals("Invalid header value.", e.getMessage());
132 }
133 try {
134 builder.setHttpMethod(null);
135 fail("Method name is not null-checked");
136 } catch (NullPointerException e) {
137 assertEquals("Method is required.", e.getMessage());
138 }
139 }
140
141 @SmallTest
142 @Feature({"Cronet"})
143 @OnlyRunNativeCronet
144 public void testFailPlainHttp() throws Exception {
145 String url = "http://example.com";
146 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
147 // Create stream.
148 BidirectionalStream stream = new BidirectionalStream
149 .Builder(url, callback, callback.ge tExecutor(),
150 mTestFramework.mCronetEngin e)
151 .build();
152 stream.start();
153 callback.blockForDone();
154 assertTrue(stream.isDone());
155 assertEquals("Exception in BidirectionalStream: net::ERR_DISALLOWED_URL_ SCHEME",
156 callback.mError.getMessage());
157 assertEquals(-301, callback.mError.netError());
158 }
159
160 @SmallTest
161 @Feature({"Cronet"})
162 @OnlyRunNativeCronet
163 public void testSimpleGet() throws Exception {
164 String url = Http2TestServer.getEchoMethodUrl();
165 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
166 // Create stream.
167 BidirectionalStream stream = new BidirectionalStream
168 .Builder(url, callback, callback.ge tExecutor(),
169 mTestFramework.mCronetEngin e)
170 .setHttpMethod("GET")
171 .build();
172 stream.start();
173 callback.blockForDone();
174 assertTrue(stream.isDone());
175 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
176 // Default method is 'GET'.
177 assertEquals("GET", callback.mResponseAsString);
178 UrlResponseInfo urlResponseInfo =
179 createUrlResponseInfo(new String[] {url}, "", 200, 27, ":status" , "200");
180 assertResponseEquals(urlResponseInfo, callback.mResponseInfo);
181 checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodU rl(), 200, "");
182 }
183
184 @SmallTest
185 @Feature({"Cronet"})
186 @OnlyRunNativeCronet
187 public void testSimpleHead() throws Exception {
188 String url = Http2TestServer.getEchoMethodUrl();
189 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
190 // Create stream.
191 BidirectionalStream stream = new BidirectionalStream
192 .Builder(url, callback, callback.ge tExecutor(),
193 mTestFramework.mCronetEngin e)
194 .setHttpMethod("HEAD")
195 .build();
196 stream.start();
197 callback.blockForDone();
198 assertTrue(stream.isDone());
199 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
200 assertEquals("HEAD", callback.mResponseAsString);
201 UrlResponseInfo urlResponseInfo =
202 createUrlResponseInfo(new String[] {url}, "", 200, 28, ":status" , "200");
203 assertResponseEquals(urlResponseInfo, callback.mResponseInfo);
204 checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodU rl(), 200, "");
205 }
206
207 @SmallTest
208 @Feature({"Cronet"})
209 @OnlyRunNativeCronet
210 public void testSimplePost() throws Exception {
211 String url = Http2TestServer.getEchoStreamUrl();
212 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
213 callback.addWriteData("Test String".getBytes());
214 callback.addWriteData("1234567890".getBytes());
215 callback.addWriteData("woot!".getBytes());
216 // Create stream.
217 BidirectionalStream stream = new BidirectionalStream
218 .Builder(url, callback, callback.ge tExecutor(),
219 mTestFramework.mCronetEngin e)
220 .addHeader("foo", "bar")
221 .addHeader("empty", "")
222 .addHeader("Content-Type", "zebra")
223 .build();
224 stream.start();
225 callback.blockForDone();
226 assertTrue(stream.isDone());
227 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
228 assertEquals("Test String1234567890woot!", callback.mResponseAsString);
229 assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo ").get(0));
230 assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty" ).get(0));
231 assertEquals(
232 "zebra", callback.mResponseInfo.getAllHeaders().get("echo-conten t-type").get(0));
233 }
234
235 @SmallTest
236 @Feature({"Cronet"})
237 @OnlyRunNativeCronet
238 public void testSimplePut() throws Exception {
239 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
240 callback.addWriteData("Put This Data!".getBytes());
241 String methodName = "PUT";
242 BidirectionalStream.Builder builder =
243 new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
244 callback.getExecutor(), mTestFramework.mCronetEngine);
245 builder.setHttpMethod(methodName);
246 builder.build().start();
247 callback.blockForDone();
248 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
249 assertEquals("Put This Data!", callback.mResponseAsString);
250 assertEquals(methodName, callback.mResponseInfo.getAllHeaders().get("ech o-method").get(0));
251 }
252
253 @SmallTest
254 @Feature({"Cronet"})
255 @OnlyRunNativeCronet
256 public void testBadMethod() throws Exception {
257 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
258 BidirectionalStream.Builder builder =
259 new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
260 callback.getExecutor(), mTestFramework.mCronetEngine);
261 try {
262 builder.setHttpMethod("bad:method!");
263 builder.build().start();
264 fail("IllegalArgumentException not thrown.");
265 } catch (IllegalArgumentException e) {
266 assertEquals("Invalid http method bad:method!", e.getMessage());
267 }
268 }
269
270 @SmallTest
271 @Feature({"Cronet"})
272 @OnlyRunNativeCronet
273 public void testBadHeaderName() throws Exception {
274 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
275 BidirectionalStream.Builder builder =
276 new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
277 callback.getExecutor(), mTestFramework.mCronetEngine);
278 try {
279 builder.addHeader("goodheader1", "headervalue");
280 builder.addHeader("header:name", "headervalue");
281 builder.addHeader("goodheader2", "headervalue");
282 builder.build().start();
283 fail("IllegalArgumentException not thrown.");
284 } catch (IllegalArgumentException e) {
285 assertEquals("Invalid header header:name=headervalue", e.getMessage( ));
286 }
287 }
288
289 @SmallTest
290 @Feature({"Cronet"})
291 @OnlyRunNativeCronet
292 public void testBadHeaderValue() throws Exception {
293 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
294 BidirectionalStream.Builder builder =
295 new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
296 callback.getExecutor(), mTestFramework.mCronetEngine);
297 try {
298 builder.addHeader("headername", "bad header\r\nvalue");
299 builder.build().start();
300 fail("IllegalArgumentException not thrown.");
301 } catch (IllegalArgumentException e) {
302 assertEquals("Invalid header headername=bad header\r\nvalue", e.getM essage());
303 }
304 }
305
306 @SmallTest
307 @Feature({"Cronet"})
308 @OnlyRunNativeCronet
309 public void testAddHeader() throws Exception {
310 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
311 String headerName = "header-name";
312 String headerValue = "header-value";
313 BidirectionalStream.Builder builder =
314 new BidirectionalStream.Builder(Http2TestServer.getEchoHeaderUrl (headerName),
315 callback, callback.getExecutor(), mTestFramework.mCronet Engine);
316 builder.addHeader(headerName, headerValue);
317 builder.setHttpMethod("GET");
318 builder.build().start();
319 callback.blockForDone();
320 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
321 assertEquals(headerValue, callback.mResponseAsString);
322 }
323
324 @SmallTest
325 @Feature({"Cronet"})
326 @OnlyRunNativeCronet
327 public void testMultiRequestHeaders() throws Exception {
328 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
329 String headerName = "header-name";
330 String headerValue1 = "header-value1";
331 String headerValue2 = "header-value2";
332 BidirectionalStream.Builder builder =
333 new BidirectionalStream.Builder(Http2TestServer.getEchoAllHeader sUrl(), callback,
334 callback.getExecutor(), mTestFramework.mCronetEngine);
335 builder.addHeader(headerName, headerValue1);
336 builder.addHeader(headerName, headerValue2);
337 builder.setHttpMethod("GET");
338 builder.build().start();
339 callback.blockForDone();
340 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
341 String headers = callback.mResponseAsString;
342 Pattern pattern = Pattern.compile(headerName + ":\\s(.*)\\r\\n");
343 Matcher matcher = pattern.matcher(headers);
344 List<String> actualValues = new ArrayList<String>();
345 while (matcher.find()) {
346 actualValues.add(matcher.group(1));
347 }
348 assertEquals(1, actualValues.size());
349 assertEquals("header-value2", actualValues.get(0));
350 }
351
352 @SmallTest
353 @Feature({"Cronet"})
354 @OnlyRunNativeCronet
355 public void testEchoTrailers() throws Exception {
356 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
357 String headerName = "header-name";
358 String headerValue = "header-value";
359 BidirectionalStream.Builder builder =
360 new BidirectionalStream.Builder(Http2TestServer.getEchoTrailersU rl(), callback,
361 callback.getExecutor(), mTestFramework.mCronetEngine);
362 builder.addHeader(headerName, headerValue);
363 builder.setHttpMethod("GET");
364 builder.build().start();
365 callback.blockForDone();
366 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
367 assertNotNull(callback.mTrailers);
368 // Verify that header value is properly echoed in trailers.
369 assertEquals(headerValue, callback.mTrailers.getAsMap().get("echo-" + he aderName).get(0));
370 }
371
372 @SmallTest
373 @Feature({"Cronet"})
374 @OnlyRunNativeCronet
375 public void testCustomUserAgent() throws Exception {
376 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
377 String userAgentName = "User-Agent";
378 String userAgentValue = "User-Agent-Value";
379 BidirectionalStream.Builder builder =
380 new BidirectionalStream.Builder(Http2TestServer.getEchoHeaderUrl (userAgentName),
381 callback, callback.getExecutor(), mTestFramework.mCronet Engine);
382 builder.setHttpMethod("GET");
383 builder.addHeader(userAgentName, userAgentValue);
384 builder.build().start();
385 callback.blockForDone();
386 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
387 assertEquals(userAgentValue, callback.mResponseAsString);
388 }
389
390 @SmallTest
391 @Feature({"Cronet"})
392 @OnlyRunNativeCronet
393 public void testEchoStream() throws Exception {
394 String url = Http2TestServer.getEchoStreamUrl();
395 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
396 String[] testData = {"Test String", createLongString("1234567890", 50000 ), "woot!"};
397 StringBuilder stringData = new StringBuilder();
398 for (String writeData : testData) {
399 callback.addWriteData(writeData.getBytes());
400 stringData.append(writeData);
401 }
402 // Create stream.
403 BidirectionalStream stream = new BidirectionalStream
404 .Builder(url, callback, callback.ge tExecutor(),
405 mTestFramework.mCronetEngin e)
406 .addHeader("foo", "Value with Space s")
407 .addHeader("Content-Type", "zebra")
408 .build();
409 stream.start();
410 callback.blockForDone();
411 assertTrue(stream.isDone());
412 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
413 assertEquals(stringData.toString(), callback.mResponseAsString);
414 assertEquals(
415 "Value with Spaces", callback.mResponseInfo.getAllHeaders().get( "echo-foo").get(0));
416 assertEquals(
417 "zebra", callback.mResponseInfo.getAllHeaders().get("echo-conten t-type").get(0));
418 }
419
420 @SmallTest
421 @Feature({"Cronet"})
422 @OnlyRunNativeCronet
423 public void testEchoStreamEmptyWrite() throws Exception {
424 String url = Http2TestServer.getEchoStreamUrl();
425 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
426 callback.addWriteData(new byte[0]);
427 // Create stream.
428 BidirectionalStream stream = new BidirectionalStream
429 .Builder(url, callback, callback.ge tExecutor(),
430 mTestFramework.mCronetEngin e)
431 .build();
432 stream.start();
433 callback.blockForDone();
434 assertTrue(stream.isDone());
435 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
436 assertEquals("", callback.mResponseAsString);
437 }
438
439 @SmallTest
440 @Feature({"Cronet"})
441 @OnlyRunNativeCronet
442 public void testDoubleWrite() throws Exception {
443 String url = Http2TestServer.getEchoStreamUrl();
444 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback() {
445 @Override
446 public void onRequestHeadersSent(BidirectionalStream stream) {
447 startNextWrite(stream);
448 try {
449 // Second write from callback invoked on single-threaded exe cutor throws
450 // an exception because first write is still pending until i ts completion
451 // is handled on executor.
452 ByteBuffer writeBuffer = ByteBuffer.allocateDirect(5);
453 writeBuffer.put("abc".getBytes());
454 writeBuffer.flip();
455 stream.write(writeBuffer, false);
456 fail("Exception is not thrown.");
457 } catch (Exception e) {
458 assertEquals("Unexpected write attempt.", e.getMessage());
459 }
460 }
461 };
462 callback.addWriteData("1".getBytes());
463 callback.addWriteData("2".getBytes());
464 // Create stream.
465 BidirectionalStream stream = new BidirectionalStream
466 .Builder(url, callback, callback.ge tExecutor(),
467 mTestFramework.mCronetEngin e)
468 .build();
469 stream.start();
470 callback.blockForDone();
471 assertTrue(stream.isDone());
472 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
473 assertEquals("12", callback.mResponseAsString);
474 }
475
476 @SmallTest
477 @Feature({"Cronet"})
478 @OnlyRunNativeCronet
479 public void testDoubleRead() throws Exception {
480 String url = Http2TestServer.getEchoStreamUrl();
481 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback() {
482 @Override
483 public void onResponseHeadersReceived(
484 BidirectionalStream stream, UrlResponseInfo info) {
485 startNextRead(stream);
486 try {
487 // Second read from callback invoked on single-threaded exec utor throws
488 // an exception because previous read is still pending until its completion
489 // is handled on executor.
490 stream.read(ByteBuffer.allocateDirect(5));
491 fail("Exception is not thrown.");
492 } catch (Exception e) {
493 assertEquals("Unexpected read attempt.", e.getMessage());
494 }
495 }
496 };
497 callback.addWriteData("1".getBytes());
498 callback.addWriteData("2".getBytes());
499 // Create stream.
500 BidirectionalStream stream = new BidirectionalStream
501 .Builder(url, callback, callback.ge tExecutor(),
502 mTestFramework.mCronetEngin e)
503 .build();
504 stream.start();
505 callback.blockForDone();
506 assertTrue(stream.isDone());
507 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
508 assertEquals("12", callback.mResponseAsString);
509 }
510
511 @SmallTest
512 @Feature({"Cronet"})
513 @OnlyRunNativeCronet
514 public void testReadAndWrite() throws Exception {
515 String url = Http2TestServer.getEchoStreamUrl();
516 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback() {
517 @Override
518 public void onResponseHeadersReceived(
519 BidirectionalStream stream, UrlResponseInfo info) {
520 // Start the write, that will not complete until callback comple tion.
521 startNextWrite(stream);
522 // Start the read. It is allowed with write in flight.
523 super.onResponseHeadersReceived(stream, info);
524 }
525 };
526 callback.setAutoAdvance(false);
527 callback.addWriteData("1".getBytes());
528 callback.addWriteData("2".getBytes());
529 // Create stream.
530 BidirectionalStream stream = new BidirectionalStream
531 .Builder(url, callback, callback.ge tExecutor(),
532 mTestFramework.mCronetEngin e)
533 .build();
534 stream.start();
535 callback.waitForNextWriteStep();
536 callback.waitForNextReadStep();
537 callback.startNextRead(stream);
538 callback.setAutoAdvance(true);
539 callback.blockForDone();
540 assertTrue(stream.isDone());
541 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
542 assertEquals("12", callback.mResponseAsString);
543 }
544
545 @SmallTest
546 @Feature({"Cronet"})
547 @OnlyRunNativeCronet
548 public void testEchoStreamWriteFirst() throws Exception {
549 String url = Http2TestServer.getEchoStreamUrl();
550 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
551 callback.setAutoAdvance(false);
552 String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woo t!"};
553 StringBuilder stringData = new StringBuilder();
554 for (String writeData : testData) {
555 callback.addWriteData(writeData.getBytes());
556 stringData.append(writeData);
557 }
558 // Create stream.
559 BidirectionalStream stream = new BidirectionalStream
560 .Builder(url, callback, callback.ge tExecutor(),
561 mTestFramework.mCronetEngin e)
562 .build();
563 stream.start();
564 // Write first.
565 callback.waitForNextWriteStep();
566 for (String expected : testData) {
567 // Write next chunk of test data.
568 callback.startNextWrite(stream);
569 callback.waitForNextWriteStep();
570 }
571
572 // Wait for read step, but don't read yet.
573 callback.waitForNextReadStep();
574 assertEquals("", callback.mResponseAsString);
575 // Read back.
576 callback.startNextRead(stream);
577 callback.waitForNextReadStep();
578 // Verify that some part of proper response is read.
579 assertTrue(callback.mResponseAsString.startsWith(testData[0]));
580 assertTrue(stringData.toString().startsWith(callback.mResponseAsString)) ;
581 // Read the rest of the response.
582 callback.setAutoAdvance(true);
583 callback.startNextRead(stream);
584 callback.blockForDone();
585 assertTrue(stream.isDone());
586 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
587 assertEquals(stringData.toString(), callback.mResponseAsString);
588 }
589
590 @SmallTest
591 @Feature({"Cronet"})
592 @OnlyRunNativeCronet
593 public void testEchoStreamStepByStep() throws Exception {
594 String url = Http2TestServer.getEchoStreamUrl();
595 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
596 callback.setAutoAdvance(false);
597 String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woo t!"};
598 StringBuilder stringData = new StringBuilder();
599 for (String writeData : testData) {
600 callback.addWriteData(writeData.getBytes());
601 stringData.append(writeData);
602 }
603 // Create stream.
604 BidirectionalStream stream = new BidirectionalStream
605 .Builder(url, callback, callback.ge tExecutor(),
606 mTestFramework.mCronetEngin e)
607 .build();
608 stream.start();
609 callback.waitForNextWriteStep();
610 callback.waitForNextReadStep();
611
612 for (String expected : testData) {
613 // Write next chunk of test data.
614 callback.startNextWrite(stream);
615 callback.waitForNextWriteStep();
616
617 // Read next chunk of test data.
618 ByteBuffer readBuffer = ByteBuffer.allocateDirect(100);
619 callback.startNextRead(stream, readBuffer);
620 callback.waitForNextReadStep();
621 assertEquals(expected.length(), readBuffer.position());
622 assertFalse(stream.isDone());
623 }
624
625 callback.setAutoAdvance(true);
626 callback.startNextRead(stream);
627 callback.blockForDone();
628 assertTrue(stream.isDone());
629 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
630 assertEquals(stringData.toString(), callback.mResponseAsString);
631 }
632
633 /**
634 * Checks that the buffer is updated correctly, when starting at an offset.
635 */
636 @SmallTest
637 @Feature({"Cronet"})
638 @OnlyRunNativeCronet
639 public void testSimpleGetBufferUpdates() throws Exception {
640 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
641 callback.setAutoAdvance(false);
642 // Since the method is "GET", the expected response body is also "GET".
643 BidirectionalStream.Builder builder =
644 new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl (), callback,
645 callback.getExecutor(), mTestFramework.mCronetEngine);
646 BidirectionalStream stream = builder.setHttpMethod("GET").build();
647 stream.start();
648 callback.waitForNextReadStep();
649
650 assertEquals(null, callback.mError);
651 assertFalse(callback.isDone());
652 assertEquals(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_ST ARTED,
653 callback.mResponseStep);
654
655 ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
656 readBuffer.put("FOR".getBytes());
657 assertEquals(3, readBuffer.position());
658
659 // Read first two characters of the response ("GE"). It's theoretically
660 // possible to need one read per character, though in practice,
661 // shouldn't happen.
662 while (callback.mResponseAsString.length() < 2) {
663 assertFalse(callback.isDone());
664 callback.startNextRead(stream, readBuffer);
665 callback.waitForNextReadStep();
666 }
667
668 // Make sure the two characters were read.
669 assertEquals("GE", callback.mResponseAsString);
670
671 // Check the contents of the entire buffer. The first 3 characters
672 // should not have been changed, and the last two should be the first
673 // two characters from the response.
674 assertEquals("FORGE", bufferContentsToString(readBuffer, 0, 5));
675 // The limit and position should be 5.
676 assertEquals(5, readBuffer.limit());
677 assertEquals(5, readBuffer.position());
678
679 assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep);
680
681 // Start reading from position 3. Since the only remaining character
682 // from the response is a "T", when the read completes, the buffer
683 // should contain "FORTE", with a position() of 4 and a limit() of 5.
684 readBuffer.position(3);
685 callback.startNextRead(stream, readBuffer);
686 callback.waitForNextReadStep();
687
688 // Make sure all three characters of the response have now been read.
689 assertEquals("GET", callback.mResponseAsString);
690
691 // Check the entire contents of the buffer. Only the third character
692 // should have been modified.
693 assertEquals("FORTE", bufferContentsToString(readBuffer, 0, 5));
694
695 // Make sure position and limit were updated correctly.
696 assertEquals(4, readBuffer.position());
697 assertEquals(5, readBuffer.limit());
698
699 assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep);
700
701 // One more read attempt. The request should complete.
702 readBuffer.position(1);
703 readBuffer.limit(5);
704 callback.startNextRead(stream, readBuffer);
705 callback.waitForNextReadStep();
706
707 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
708 assertEquals("GET", callback.mResponseAsString);
709 checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodU rl(), 200, "");
710
711 // Check that buffer contents were not modified.
712 assertEquals("FORTE", bufferContentsToString(readBuffer, 0, 5));
713
714 // Position should not have been modified, since nothing was read.
715 assertEquals(1, readBuffer.position());
716 // Limit should be unchanged as always.
717 assertEquals(5, readBuffer.limit());
718
719 assertEquals(ResponseStep.ON_SUCCEEDED, callback.mResponseStep);
720
721 // Make sure there are no other pending messages, which would trigger
722 // asserts in TestBidirectionalCallback.
723 testSimpleGet();
724 }
725
726 @SmallTest
727 @Feature({"Cronet"})
728 @OnlyRunNativeCronet
729 public void testBadBuffers() throws Exception {
730 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
731 callback.setAutoAdvance(false);
732 BidirectionalStream.Builder builder =
733 new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl (), callback,
734 callback.getExecutor(), mTestFramework.mCronetEngine);
735 BidirectionalStream stream = builder.setHttpMethod("GET").build();
736 stream.start();
737 callback.waitForNextReadStep();
738
739 assertEquals(null, callback.mError);
740 assertFalse(callback.isDone());
741 assertEquals(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_ST ARTED,
742 callback.mResponseStep);
743
744 // Try to read using a full buffer.
745 try {
746 ByteBuffer readBuffer = ByteBuffer.allocateDirect(4);
747 readBuffer.put("full".getBytes());
748 stream.read(readBuffer);
749 fail("Exception not thrown");
750 } catch (IllegalArgumentException e) {
751 assertEquals("ByteBuffer is already full.", e.getMessage());
752 }
753
754 // Try to read using a non-direct buffer.
755 try {
756 ByteBuffer readBuffer = ByteBuffer.allocate(5);
757 stream.read(readBuffer);
758 fail("Exception not thrown");
759 } catch (Exception e) {
760 assertEquals("byteBuffer must be a direct ByteBuffer.", e.getMessage ());
761 }
762
763 // Finish the stream with a direct ByteBuffer.
764 callback.setAutoAdvance(true);
765 ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
766 stream.read(readBuffer);
767 callback.blockForDone();
768 assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
769 assertEquals("GET", callback.mResponseAsString);
770 }
771
772 private void throwOrCancel(FailureType failureType, ResponseStep failureStep ,
773 boolean expectResponseInfo, boolean expectError) {
774 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
775 callback.setFailure(failureType, failureStep);
776 BidirectionalStream.Builder builder =
777 new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl (), callback,
778 callback.getExecutor(), mTestFramework.mCronetEngine);
779 BidirectionalStream stream = builder.setHttpMethod("GET").build();
780 stream.start();
781 callback.blockForDone();
782 // assertEquals(callback.mResponseStep, failureStep);
783 assertTrue(stream.isDone());
784 assertEquals(expectResponseInfo, callback.mResponseInfo != null);
785 assertEquals(expectError, callback.mError != null);
786 assertEquals(expectError, callback.mOnErrorCalled);
787 assertEquals(failureType == FailureType.CANCEL_SYNC
788 || failureType == FailureType.CANCEL_ASYNC
789 || failureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE ,
790 callback.mOnCanceledCalled);
791 }
792
793 @SmallTest
794 @Feature({"Cronet"})
795 @OnlyRunNativeCronet
796 public void testFailures() throws Exception {
797 throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_REQUEST_HEADERS_S ENT, false, false);
798 throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_REQUEST_HEADERS_ SENT, false, false);
799 throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_RE QUEST_HEADERS_SENT,
800 false, false);
801 throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_REQUEST_HEADERS_SE NT, false, true);
802
803 throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_RESPONSE_STARTED, true, false);
804 throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_RESPONSE_STARTED , true, false);
805 throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_RE SPONSE_STARTED,
806 true, false);
807 throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_RESPONSE_STARTED, true, true);
808
809 throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_READ_COMPLETED, t rue, false);
810 throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_READ_COMPLETED, true, false);
811 throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_RE AD_COMPLETED, true,
812 false);
813 throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED, tr ue, true);
814 }
815
816 @SmallTest
817 @Feature({"Cronet"})
818 @OnlyRunNativeCronet
819 public void testThrowOnSucceeded() {
820 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
821 callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_SUCCEEDED);
822 BidirectionalStream.Builder builder =
823 new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl (), callback,
824 callback.getExecutor(), mTestFramework.mCronetEngine);
825 BidirectionalStream stream = builder.setHttpMethod("GET").build();
826 stream.start();
827 callback.blockForDone();
828 assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED);
829 assertTrue(stream.isDone());
830 assertNotNull(callback.mResponseInfo);
831 // Check that error thrown from 'onSucceeded' callback is not reported.
832 assertNull(callback.mError);
833 assertFalse(callback.mOnErrorCalled);
834 }
835
836 @SmallTest
837 @Feature({"Cronet"})
838 @OnlyRunNativeCronet
839 public void testExecutorShutdownBeforeStreamIsDone() {
840 // Test that stream is destroyed even if executor is shut down and rejec ts posting tasks.
841 TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCa llback();
842 callback.setAutoAdvance(false);
843 BidirectionalStream.Builder builder =
844 new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl (), callback,
845 callback.getExecutor(), mTestFramework.mCronetEngine);
846 CronetBidirectionalStream stream =
847 (CronetBidirectionalStream) builder.setHttpMethod("GET").build() ;
848 stream.start();
849 callback.waitForNextReadStep();
850 assertFalse(callback.isDone());
851 assertFalse(stream.isDone());
852
853 final ConditionVariable streamDestroyed = new ConditionVariable(false);
854 stream.setOnDestroyedCallbackForTesting(new Runnable() {
855 @Override
856 public void run() {
857 streamDestroyed.open();
858 }
859 });
860
861 // Shut down the executor, so posting the task will throw an exception.
862 callback.shutdownExecutor();
863 ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
864 stream.read(readBuffer);
865 // Callback will never be called again because executor is shut down,
866 // but stream will be destroyed from network thread.
867 streamDestroyed.block();
868
869 assertFalse(callback.isDone());
870 assertTrue(stream.isDone());
871 }
872
873 /**
874 * Callback that shuts down the engine when the stream has succeeded
875 * or failed.
876 */
877 private class ShutdownTestBidirectionalStreamCallback extends TestBidirectio nalStreamCallback {
878 @Override
879 public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info ) {
880 mTestFramework.mCronetEngine.shutdown();
881 // Clear mCronetEngine so it doesn't get shut down second time in te arDown().
882 mTestFramework.mCronetEngine = null;
883 super.onSucceeded(stream, info);
884 }
885
886 @Override
887 public void onFailed(
888 BidirectionalStream stream, UrlResponseInfo info, CronetExceptio n error) {
889 mTestFramework.mCronetEngine.shutdown();
890 // Clear mCronetEngine so it doesn't get shut down second time in te arDown().
891 mTestFramework.mCronetEngine = null;
892 super.onFailed(stream, info, error);
893 }
894
895 @Override
896 public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) {
897 mTestFramework.mCronetEngine.shutdown();
898 // Clear mCronetEngine so it doesn't get shut down second time in te arDown().
899 mTestFramework.mCronetEngine = null;
900 super.onCanceled(stream, info);
901 }
902 }
903
904 @SmallTest
905 @Feature({"Cronet"})
906 @OnlyRunNativeCronet
907 public void testCronetEngineShutdown() throws Exception {
908 // Test that CronetEngine cannot be shut down if there are any active st reams.
909 TestBidirectionalStreamCallback callback = new ShutdownTestBidirectional StreamCallback();
910 // Block callback when response starts to verify that shutdown fails
911 // if there are active streams.
912 callback.setAutoAdvance(false);
913 BidirectionalStream.Builder builder =
914 new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl (), callback,
915 callback.getExecutor(), mTestFramework.mCronetEngine);
916 CronetBidirectionalStream stream =
917 (CronetBidirectionalStream) builder.setHttpMethod("GET").build() ;
918 stream.start();
919 try {
920 mTestFramework.mCronetEngine.shutdown();
921 fail("Should throw an exception");
922 } catch (Exception e) {
923 assertEquals("Cannot shutdown with active requests.", e.getMessage() );
924 }
925
926 callback.waitForNextReadStep();
927 assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep);
928 try {
929 mTestFramework.mCronetEngine.shutdown();
930 fail("Should throw an exception");
931 } catch (Exception e) {
932 assertEquals("Cannot shutdown with active requests.", e.getMessage() );
933 }
934 callback.startNextRead(stream);
935
936 callback.waitForNextReadStep();
937 assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep);
938 try {
939 mTestFramework.mCronetEngine.shutdown();
940 fail("Should throw an exception");
941 } catch (Exception e) {
942 assertEquals("Cannot shutdown with active requests.", e.getMessage() );
943 }
944
945 // May not have read all the data, in theory. Just enable auto-advance
946 // and finish the request.
947 callback.setAutoAdvance(true);
948 callback.startNextRead(stream);
949 callback.blockForDone();
950 }
951
952 @SmallTest
953 @Feature({"Cronet"})
954 @OnlyRunNativeCronet
955 public void testCronetEngineShutdownAfterStreamFailure() throws Exception {
956 // Test that CronetEngine can be shut down after stream reports a failur e.
957 TestBidirectionalStreamCallback callback = new ShutdownTestBidirectional StreamCallback();
958 BidirectionalStream.Builder builder =
959 new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl (), callback,
960 callback.getExecutor(), mTestFramework.mCronetEngine);
961 CronetBidirectionalStream stream =
962 (CronetBidirectionalStream) builder.setHttpMethod("GET").build() ;
963 stream.start();
964 callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLET ED);
965 callback.blockForDone();
966 assertTrue(callback.mOnErrorCalled);
967 assertNull(mTestFramework.mCronetEngine);
968 }
969
970 @SmallTest
971 @Feature({"Cronet"})
972 @OnlyRunNativeCronet
973 public void testCronetEngineShutdownAfterStreamCancel() throws Exception {
974 // Test that CronetEngine can be shut down after stream is canceled.
975 TestBidirectionalStreamCallback callback = new ShutdownTestBidirectional StreamCallback();
976 BidirectionalStream.Builder builder =
977 new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl (), callback,
978 callback.getExecutor(), mTestFramework.mCronetEngine);
979 CronetBidirectionalStream stream =
980 (CronetBidirectionalStream) builder.setHttpMethod("GET").build() ;
981
982 // Block callback when response starts to verify that shutdown fails
983 // if there are active requests.
984 callback.setAutoAdvance(false);
985 stream.start();
986 try {
987 mTestFramework.mCronetEngine.shutdown();
988 fail("Should throw an exception");
989 } catch (Exception e) {
990 assertEquals("Cannot shutdown with active requests.", e.getMessage() );
991 }
992 callback.waitForNextReadStep();
993 assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep);
994 stream.cancel();
995 callback.blockForDone();
996 assertTrue(callback.mOnCanceledCalled);
997 assertNull(mTestFramework.mCronetEngine);
998 }
999
1000 // Returns the contents of byteBuffer, from its position() to its limit(),
1001 // as a String. Does not modify byteBuffer's position().
1002 private static String bufferContentsToString(ByteBuffer byteBuffer, int star t, int end) {
1003 // Use a duplicate to avoid modifying byteBuffer.
1004 ByteBuffer duplicate = byteBuffer.duplicate();
1005 duplicate.position(start);
1006 duplicate.limit(end);
1007 byte[] contents = new byte[duplicate.remaining()];
1008 duplicate.get(contents);
1009 return new String(contents);
120 } 1010 }
121 } 1011 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698