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

Side by Side Diff: components/cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java

Issue 1412243012: Initial implementation of CronetBidirectionalStream. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Address Paul's comments. Created 4 years, 11 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
(Empty)
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
3 // found in the LICENSE file.
4
5 package org.chromium.net;
6
7 import org.chromium.base.Log;
8 import org.chromium.base.VisibleForTesting;
9 import org.chromium.base.annotations.CalledByNative;
10 import org.chromium.base.annotations.JNINamespace;
11 import org.chromium.base.annotations.NativeClassQualifiedName;
12
13 import java.nio.ByteBuffer;
14 import java.util.AbstractMap;
15 import java.util.ArrayList;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.concurrent.Executor;
19 import java.util.concurrent.RejectedExecutionException;
20
21 import javax.annotation.concurrent.GuardedBy;
22
23 /**
24 * BidirectionalStream implementation using Chromium network stack.
25 * All @CallByNative methods are called on native network thread
26 * and post tasks with callback calls onto Executor. Upon return from callback n ative
27 * stream is called on executor thread and posts native tasks to native network thread.
28 */
29 @JNINamespace("cronet")
30 class CronetBidirectionalStream extends BidirectionalStream {
31 /**
32 * States of BidirectionalStream are tracked in mReadState and mWriteState.
33 * The write state is separated out as it changes independently of the strea m state.
34 * There is one initial state - State.NOT_STARTED. There is one final state - State.SUCCESS,
35 * reached after State.READING_DONE and State.WRITING_DONE. There are 2 exce ption states -
36 * State.CANCELED and State.ERROR, which can be reached from any other state except
37 * State.SUCCESS.
38 */
39 private enum State {
40 /* Initial state, stream not started. */
41 NOT_STARTED,
42 /* Stream started, request headers are sent. */
43 STARTED,
44 /* Waiting for {@code read()} to be called. */
45 WAITING_ON_READ,
46 /* Reading from the remote, {@code onReadCompleted()} callback will be c alled when done. */
47 READING,
48 /* There is no more data to read and stream is half-closed by the remote side. */
49 READING_DONE,
50 /* Stream is canceled. */
51 CANCELED,
52 /* Error has occured, stream is closed. */
53 ERROR,
54 /* Reading and writing is done, and the stream is closed successfully. * /
55 SUCCESS,
56 /* Waiting for {@code write()} to be called. */
57 WAITING_ON_WRITE,
58 /* Writing to the remote, {@code onWriteCompleted()} callback will be ca lled when done. */
59 WRITING,
60 /* Writing the last frame, so {@code State.WRITING_DONE} will be set upo n completion. */
61 WRITING_END_OF_STREAM,
62 /* There is no more data to write and stream is half-closed by the local side. */
63 WRITING_DONE,
64 }
65
66 private final CronetUrlRequestContext mRequestContext;
67 private final Executor mExecutor;
68 private final Callback mCallback;
69 private final String mInitialUrl;
70 private final int mInitialPriority;
71 private final String mInitialMethod;
72 private final ArrayList<Map.Entry<String, String>> mRequestHeaders;
73
74 /*
75 * Synchronize access to mNativeStream, mReadState and mWriteState.
76 */
77 private final Object mNativeStreamLock = new Object();
78
79 /* Native BidirectionalStream object, owned by CronetBidirectionalStream. */
80 @GuardedBy("mNativeStreamLock") private long mNativeStream;
81
82 /**
83 * Read state is tracking reading flow.
84 * / <--- READING <--- \
85 * | |
86 * \ /
87 * NOT_STARTED -> STARTED --> WAITING_ON_READ -> READING_DONE -> SUCCESS
88 */
89 @GuardedBy("mNativeStreamLock") private State mReadState = State.NOT_STARTED ;
90
91 /**
92 * Write state is tracking writing flow.
93 * / <--- WRITING <--- \
94 * | |
95 * \ /
96 * NOT_STARTED -> STARTED --> WAITING_ON_WRITE -> WRITING_END_OF_STREAM -> W RITING_DONE ->
97 * SUCCESS
98 */
99 @GuardedBy("mNativeStreamLock") private State mWriteState = State.NOT_STARTE D;
100
101 private UrlResponseInfo mResponseInfo;
102
103 /*
104 * OnReadCompleted callback is repeatedly invoked when each read is complete d, so it
105 * is cached as a member variable.
106 */
107 private OnReadCompletedRunnable mOnReadCompletedTask;
108
109 /*
110 * OnWriteCompleted callback is repeatedly invoked when each write is comple ted, so it
111 * is cached as a member variable.
112 */
113 private OnWriteCompletedRunnable mOnWriteCompletedTask;
114
115 private Runnable mOnDestroyedCallbackForTesting;
116
117 private final class OnReadCompletedRunnable implements Runnable {
118 // Buffer passed back from current invocation of onReadCompleted.
119 ByteBuffer mByteBuffer;
120 // End of stream flag from current invocation of onReadCompleted.
121 boolean mEndOfStream;
122
123 @Override
124 public void run() {
125 try {
126 // Null out mByteBuffer, to pass buffer ownership to callback or release if done.
127 ByteBuffer buffer = mByteBuffer;
128 mByteBuffer = null;
129 synchronized (mNativeStreamLock) {
130 if (isDoneLocked()) {
131 return;
132 }
133 if (mEndOfStream) {
134 mReadState = State.READING_DONE;
135 if (maybeSucceeded()) {
136 return;
137 }
138 } else {
139 mReadState = State.WAITING_ON_READ;
140 }
141 }
142 mCallback.onReadCompleted(CronetBidirectionalStream.this, mRespo nseInfo, buffer);
143 } catch (Exception e) {
144 onCallbackException(e);
145 }
146 }
147 }
148
149 private final class OnWriteCompletedRunnable implements Runnable {
150 // Buffer passed back from current invocation of onWriteCompleted.
151 ByteBuffer mByteBuffer;
152
153 @Override
154 public void run() {
155 try {
156 // Null out mByteBuffer, to pass buffer ownership to callback or release if done.
157 ByteBuffer buffer = mByteBuffer;
158 mByteBuffer = null;
159 synchronized (mNativeStreamLock) {
160 if (isDoneLocked()) {
161 return;
162 }
163 if (mWriteState == State.WRITING_END_OF_STREAM) {
164 mWriteState = State.WRITING_DONE;
165 if (maybeSucceeded()) {
166 return;
167 }
168 } else {
169 mWriteState = State.WAITING_ON_WRITE;
170 }
171 }
172 mCallback.onWriteCompleted(CronetBidirectionalStream.this, mResp onseInfo, buffer);
173 } catch (Exception e) {
174 onCallbackException(e);
175 }
176 }
177 }
178
179 CronetBidirectionalStream(CronetUrlRequestContext requestContext, long urlRe questContextAdapter,
180 String url, @BidirectionalStream.Builder.StreamPriority int priority , Callback callback,
181 Executor executor, String httpMethod, List<Map.Entry<String, String> > requestHeaders) {
182 mRequestContext = requestContext;
183 mInitialUrl = url;
184 mInitialPriority = convertStreamPriority(priority);
185 mCallback = callback;
186 mExecutor = executor;
187 mInitialMethod = httpMethod;
188 mRequestHeaders = new ArrayList<Map.Entry<String, String>>(requestHeader s);
189 }
190
191 @GuardedBy("mNativeStreamLock")
192 private boolean maybeSucceeded() {
193 if (mReadState != State.READING_DONE || mWriteState != State.WRITING_DON E) {
194 return false;
195 }
196
197 mReadState = mWriteState = State.SUCCESS;
198 Runnable task = new Runnable() {
199 public void run() {
200 synchronized (mNativeStreamLock) {
201 if (isDoneLocked()) {
202 return;
203 }
204 // Destroy native stream first, so request context could be shut
205 // down from the listener.
206 destroyNativeStream(false);
207 }
208 try {
209 mCallback.onSucceeded(CronetBidirectionalStream.this, mRespo nseInfo);
210 } catch (Exception e) {
211 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onSucce eded method", e);
212 }
213 }
214 };
215 postTaskToExecutor(task);
216 return true;
217 }
218
219 private static boolean doesMethodAllowWriteData(String methodName) {
220 return !methodName.equals("GET") && !methodName.equals("HEAD");
221 }
222
223 @Override
224 public void start() {
225 synchronized (mNativeStreamLock) {
226 if (mReadState != State.NOT_STARTED) {
227 throw new IllegalStateException("Stream is already started.");
228 }
229 try {
230 mNativeStream = nativeCreateBidirectionalStream(
231 mRequestContext.getUrlRequestContextAdapter());
232 mRequestContext.onRequestStarted();
233 String headers[] = stringsFromHeaderList(mRequestHeaders);
234 // Non-zero startResult means an argument error.
235 int startResult = nativeStart(mNativeStream, mInitialUrl, mIniti alPriority,
236 mInitialMethod, headers, !doesMethodAllowWriteData(mInit ialMethod));
237 if (startResult == -1) {
238 throw new IllegalArgumentException("Invalid http method " + mInitialMethod);
239 }
240 if (startResult > 0) {
241 int headerPos = startResult - 1;
242 throw new IllegalArgumentException(
243 "Invalid header " + headers[headerPos] + "=" + heade rs[headerPos + 1]);
244 }
245 mReadState = mWriteState = State.STARTED;
246 } catch (RuntimeException e) {
247 // If there's an exception, cleanup and then throw the
248 // exception to the caller.
249 destroyNativeStream(false);
250 throw e;
251 }
252 }
253 }
254
255 @Override
256 public void read(ByteBuffer buffer) {
257 synchronized (mNativeStreamLock) {
258 if (!buffer.hasRemaining()) {
259 throw new IllegalArgumentException("ByteBuffer is already full." );
260 }
261 if (mReadState != State.WAITING_ON_READ) {
262 throw new IllegalStateException("Unexpected read attempt.");
263 }
264 if (isDoneLocked()) {
265 return;
266 }
267 mReadState = State.READING;
268 if (!nativeReadData(mNativeStream, buffer, buffer.position(), buffer .limit())) {
269 // Still waiting on read. This is just to have consistent
270 // behavior with the other error cases.
271 mReadState = State.WAITING_ON_READ;
272 // Since accessing byteBuffer's memory failed, it's presumably
273 // not a direct ByteBuffer.
274 throw new IllegalArgumentException("byteBuffer must be a direct ByteBuffer.");
275 }
276 }
277 }
278
279 @Override
280 public void write(ByteBuffer buffer, boolean endOfStream) {
281 synchronized (mNativeStreamLock) {
282 if (!buffer.hasRemaining() && !endOfStream) {
283 throw new IllegalArgumentException("Empty buffer before end of s tream.");
284 }
285 if (mWriteState != State.WAITING_ON_WRITE) {
286 throw new IllegalStateException("Unexpected write attempt.");
287 }
288 if (isDoneLocked()) {
289 return;
290 }
291 mWriteState = endOfStream ? State.WRITING_END_OF_STREAM : State.WRIT ING;
292 if (!nativeWriteData(
293 mNativeStream, buffer, buffer.position(), buffer.limit() , endOfStream)) {
294 // Still waiting on write. This is just to have consistent
295 // behavior with the other error cases.
296 mWriteState = State.WAITING_ON_WRITE;
297 // Since accessing byteBuffer's memory failed, it's presumably
298 // not a direct ByteBuffer.
299 throw new IllegalArgumentException("byteBuffer must be a direct ByteBuffer.");
300 }
301 }
302 }
303
304 @Override
305 public void ping(PingCallback callback, Executor executor) {
306 // TODO(mef): May be last thing to be implemented on Android.
307 throw new UnsupportedOperationException("ping is not supported yet.");
308 }
309
310 @Override
311 public void windowUpdate(int windowSizeIncrement) {
312 // TODO(mef): Understand the needs and semantics of this method.
313 throw new UnsupportedOperationException("windowUpdate is not supported y et.");
314 }
315
316 @Override
317 public void cancel() {
318 synchronized (mNativeStreamLock) {
319 if (isDoneLocked() || mReadState == State.NOT_STARTED) {
320 return;
321 }
322 mReadState = mWriteState = State.CANCELED;
323 destroyNativeStream(true);
324 }
325 }
326
327 @Override
328 public boolean isDone() {
329 synchronized (mNativeStreamLock) {
330 return isDoneLocked();
331 }
332 }
333
334 @GuardedBy("mNativeStreamLock")
335 private boolean isDoneLocked() {
336 return mReadState != State.NOT_STARTED && mNativeStream == 0;
337 }
338
339 @SuppressWarnings("unused")
340 @CalledByNative
341 private void onRequestHeadersSent() {
342 Runnable task = new Runnable() {
343 public void run() {
344 synchronized (mNativeStreamLock) {
345 if (isDoneLocked()) {
346 return;
347 }
348 if (doesMethodAllowWriteData(mInitialMethod)) {
349 mWriteState = State.WAITING_ON_WRITE;
350 } else {
351 mWriteState = State.WRITING_DONE;
352 }
353 }
354
355 try {
356 mCallback.onRequestHeadersSent(CronetBidirectionalStream.thi s);
357 } catch (Exception e) {
358 onCallbackException(e);
359 }
360 }
361 };
362 postTaskToExecutor(task);
363 }
364
365 /**
366 * Called when the final set of headers, after all redirects,
367 * is received. Can only be called once for each stream.
368 */
369 @SuppressWarnings("unused")
370 @CalledByNative
371 private void onResponseHeadersReceived(int httpStatusCode, String[] headers) {
372 mResponseInfo = prepareResponseInfoOnNetworkThread(httpStatusCode, heade rs);
373 Runnable task = new Runnable() {
374 public void run() {
375 synchronized (mNativeStreamLock) {
376 if (isDoneLocked()) {
377 return;
378 }
379 mReadState = State.WAITING_ON_READ;
380 }
381
382 try {
383 mCallback.onResponseHeadersReceived(
384 CronetBidirectionalStream.this, mResponseInfo);
385 } catch (Exception e) {
386 onCallbackException(e);
387 }
388 }
389 };
390 postTaskToExecutor(task);
391 }
392
393 @SuppressWarnings("unused")
394 @CalledByNative
395 private void onReadCompleted(final ByteBuffer byteBuffer, int bytesRead, int initialPosition,
396 int initialLimit, long receivedBytesCount) {
397 mResponseInfo.setReceivedBytesCount(receivedBytesCount);
398 if (byteBuffer.position() != initialPosition || byteBuffer.limit() != in itialLimit) {
399 failWithException(
400 new CronetException("ByteBuffer modified externally during r ead", null));
401 return;
402 }
403 if (bytesRead < 0 || initialPosition + bytesRead > byteBuffer.limit()) {
404 failWithException(new CronetException("Invalid number of bytes read" , null));
405 return;
406 }
407 if (mOnReadCompletedTask == null) {
408 mOnReadCompletedTask = new OnReadCompletedRunnable();
409 }
410 byteBuffer.position(initialPosition + bytesRead);
411 mOnReadCompletedTask.mByteBuffer = byteBuffer;
412 mOnReadCompletedTask.mEndOfStream = (bytesRead == 0);
413 postTaskToExecutor(mOnReadCompletedTask);
414 }
415
416 @SuppressWarnings("unused")
417 @CalledByNative
418 private void onWriteCompleted(
419 final ByteBuffer byteBuffer, int initialPosition, int initialLimit) {
420 if (byteBuffer.position() != initialPosition || byteBuffer.limit() != in itialLimit) {
421 failWithException(
422 new CronetException("ByteBuffer modified externally during w rite", null));
423 return;
424 }
425 if (mOnWriteCompletedTask == null) {
426 mOnWriteCompletedTask = new OnWriteCompletedRunnable();
427 }
428 // Current implementation always writes the complete buffer.
429 byteBuffer.position(byteBuffer.limit());
430 mOnWriteCompletedTask.mByteBuffer = byteBuffer;
431 postTaskToExecutor(mOnWriteCompletedTask);
432 }
433
434 @SuppressWarnings("unused")
435 @CalledByNative
436 private void onResponseTrailersReceived(String[] trailers) {
437 final UrlResponseInfo.HeaderBlock trailersBlock =
438 new UrlResponseInfo.HeaderBlock(headersListFromStrings(trailers) );
439 Runnable task = new Runnable() {
440 public void run() {
441 synchronized (mNativeStreamLock) {
442 if (isDoneLocked()) {
443 return;
444 }
445 }
446 try {
447 mCallback.onResponseTrailersReceived(
448 CronetBidirectionalStream.this, mResponseInfo, trail ersBlock);
449 } catch (Exception e) {
450 onCallbackException(e);
451 }
452 }
453 };
454 postTaskToExecutor(task);
455 }
456
457 @SuppressWarnings("unused")
458 @CalledByNative
459 private void onError(final int nativeError, final String errorString, long r eceivedBytesCount) {
460 if (mResponseInfo != null) {
461 mResponseInfo.setReceivedBytesCount(receivedBytesCount);
462 }
463 failWithException(new CronetException(
464 "Exception in BidirectionalStream: " + errorString, nativeError) );
465 }
466
467 /**
468 * Called when request is canceled, no callbacks will be called afterwards.
469 */
470 @SuppressWarnings("unused")
471 @CalledByNative
472 private void onCanceled() {
473 Runnable task = new Runnable() {
474 public void run() {
475 try {
476 mCallback.onCanceled(CronetBidirectionalStream.this, mRespon seInfo);
477 } catch (Exception e) {
478 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onCance led method", e);
479 }
480 }
481 };
482 postTaskToExecutor(task);
483 }
484
485 @VisibleForTesting
486 public void setOnDestroyedCallbackForTesting(Runnable onDestroyedCallbackFor Testing) {
487 mOnDestroyedCallbackForTesting = onDestroyedCallbackForTesting;
488 }
489
490 /**
491 * Posts task to application Executor. Used for callbacks
492 * and other tasks that should not be executed on network thread.
493 */
494 private void postTaskToExecutor(Runnable task) {
495 try {
496 mExecutor.execute(task);
497 } catch (RejectedExecutionException failException) {
498 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to ex ecutor",
499 failException);
500 // If posting a task throws an exception, then there is no choice
501 // but to cancel the stream.
502 cancel();
pauljensen 2016/01/12 16:55:41 I don't think we want to trigger an onCancelled ca
mef 2016/01/14 21:07:54 Hrm, I disagree. :) Let's chat about it, but what
mef 2016/01/15 17:56:06 Discussed. Changed. Done.
503 }
504 }
505
506 private static ArrayList<Map.Entry<String, String>> headersListFromStrings(S tring[] headers) {
507 ArrayList<Map.Entry<String, String>> headersList =
508 new ArrayList<Map.Entry<String, String>>(headers.length / 2);
509 for (int i = 0; i < headers.length; i += 2) {
510 headersList.add(new AbstractMap.SimpleImmutableEntry<String, String> (
511 headers[i], headers[i + 1]));
512 }
513 return headersList;
514 }
515
516 private static String[] stringsFromHeaderList(
517 ArrayList<Map.Entry<String, String>> headersList) {
518 String headersArray[] = new String[headersList.size() * 2];
519 int i = 0;
520 for (Map.Entry<String, String> requestHeader : headersList) {
521 headersArray[i++] = requestHeader.getKey();
522 headersArray[i++] = requestHeader.getValue();
523 }
524 return headersArray;
525 }
526
527 private UrlResponseInfo prepareResponseInfoOnNetworkThread(
528 int httpStatusCode, String[] headers) {
529 long nativeStream;
530 synchronized (mNativeStreamLock) {
531 if (mNativeStream == 0) {
532 return null;
533 }
534 // This method is running on network thread, so even if
535 // mNativeStream is set to 0 from another thread the actual
536 // deletion of the adapter is posted to network thread, so it is
537 // safe to preserve and use nativeStream outside the lock.
538 nativeStream = mNativeStream;
539 }
540
541 ArrayList<String> urlChain = new ArrayList<String>();
542 urlChain.add(mInitialUrl);
543
544 boolean wasCached = false;
545 String httpStatusText = "";
546 String negotiatedProtocol = nativeGetNegotiatedProtocol(nativeStream);
547 String proxyServer = null;
548
549 UrlResponseInfo responseInfo = new UrlResponseInfo(urlChain, httpStatusC ode, httpStatusText,
550 headersListFromStrings(headers), wasCached, negotiatedProtocol, proxyServer);
551 return responseInfo;
552 }
553
554 private static int convertStreamPriority(
555 @BidirectionalStream.Builder.StreamPriority int priority) {
556 switch (priority) {
557 case Builder.STREAM_PRIORITY_IDLE:
558 return RequestPriority.IDLE;
559 case Builder.STREAM_PRIORITY_LOWEST:
560 return RequestPriority.LOWEST;
561 case Builder.STREAM_PRIORITY_LOW:
562 return RequestPriority.LOW;
563 case Builder.STREAM_PRIORITY_MEDIUM:
564 return RequestPriority.MEDIUM;
565 case Builder.STREAM_PRIORITY_HIGHEST:
566 return RequestPriority.HIGHEST;
567 default:
568 return RequestPriority.MEDIUM;
569 }
570 }
571
572 private void destroyNativeStream(boolean sendOnCanceled) {
573 synchronized (mNativeStreamLock) {
574 Log.i(CronetUrlRequestContext.LOG_TAG, "destroyNativeStream " + this .toString());
575 if (mNativeStream == 0) {
576 return;
577 }
578 nativeDestroy(mNativeStream, sendOnCanceled);
579 mNativeStream = 0;
580 mRequestContext.onRequestDestroyed();
581 if (mOnDestroyedCallbackForTesting != null) {
582 mOnDestroyedCallbackForTesting.run();
583 }
584 }
585 }
586
587 /**
588 * Fails the stream with an exception. Only called on the Executor.
589 */
590 private void failWithExceptionOnExecutor(CronetException e) {
591 // Do not call into listener if request is complete.
592 synchronized (mNativeStreamLock) {
593 if (isDoneLocked()) {
594 return;
595 }
596 mReadState = mWriteState = State.ERROR;
597 destroyNativeStream(false);
598 }
599 try {
600 mCallback.onFailed(this, mResponseInfo, e);
601 } catch (Exception failException) {
602 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception notifying of faile d request",
603 failException);
604 }
605 }
606
607 /**
608 * If callback method throws an exception, stream gets canceled
609 * and exception is reported via onFailed callback.
610 * Only called on the Executor.
611 */
612 private void onCallbackException(Exception e) {
613 CronetException streamError =
614 new CronetException("CalledByNative method has thrown an excepti on", e);
615 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in CalledByNative meth od", e);
616 failWithExceptionOnExecutor(streamError);
617 }
618
619 /**
620 * Fails the stream with an exception. Can be called on any thread.
621 */
622 private void failWithException(final CronetException exception) {
623 Runnable task = new Runnable() {
624 public void run() {
625 failWithExceptionOnExecutor(exception);
626 }
627 };
628 postTaskToExecutor(task);
629 }
630
631 // Native methods are implemented in cronet_bidirectional_stream_adapter.cc.
632 private native long nativeCreateBidirectionalStream(long urlRequestContextAd apter);
633
634 @NativeClassQualifiedName("CronetBidirectionalStreamAdapter")
635 private native int nativeStart(long nativePtr, String url, int priority, Str ing method,
636 String[] headers, boolean endOfStream);
637
638 @NativeClassQualifiedName("CronetBidirectionalStreamAdapter")
639 private native boolean nativeReadData(
640 long nativePtr, ByteBuffer byteBuffer, int position, int limit);
641
642 @NativeClassQualifiedName("CronetBidirectionalStreamAdapter")
643 private native boolean nativeWriteData(
644 long nativePtr, ByteBuffer byteBuffer, int position, int limit, bool ean endOfStream);
645
646 @NativeClassQualifiedName("CronetBidirectionalStreamAdapter")
647 private native void nativeDestroy(long nativePtr, boolean sendOnCanceled);
648
649 @NativeClassQualifiedName("CronetBidirectionalStreamAdapter")
650 private native String nativeGetNegotiatedProtocol(long nativePtr);
651 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698