summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormef <mef@chromium.org>2016-01-29 16:13:45 -0800
committerCommit bot <commit-bot@chromium.org>2016-01-30 00:14:47 +0000
commit72e10340f0078cc85d10ae9654cec3e91b46b128 (patch)
tree595959a6a5d54f5a8074f903c06144bd587394cc
parentb69e64de8c10dee033d42e8cd92dcca6f2f81411 (diff)
downloadchromium_src-72e10340f0078cc85d10ae9654cec3e91b46b128.zip
chromium_src-72e10340f0078cc85d10ae9654cec3e91b46b128.tar.gz
chromium_src-72e10340f0078cc85d10ae9654cec3e91b46b128.tar.bz2
Initial implementation of CronetBidirectionalStream.
BUG=516342 Review URL: https://codereview.chromium.org/1412243012 Cr-Commit-Position: refs/heads/master@{#372486}
-rw-r--r--components/cronet.gypi26
-rw-r--r--components/cronet/android/BUILD.gn86
-rw-r--r--components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java2
-rw-r--r--components/cronet/android/api/src/org/chromium/net/CronetException.java2
-rw-r--r--components/cronet/android/cronet_bidirectional_stream_adapter.cc349
-rw-r--r--components/cronet/android/cronet_bidirectional_stream_adapter.h130
-rw-r--r--components/cronet/android/cronet_library_loader.cc9
-rw-r--r--components/cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java644
-rw-r--r--components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java4
-rw-r--r--components/cronet/android/java/src/org/chromium/net/CronetUrlRequestContext.java10
-rw-r--r--components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java1002
-rw-r--r--components/cronet/android/test/javatests/src/org/chromium/net/CronetTestBase.java16
-rw-r--r--components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java16
-rw-r--r--components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java5
-rw-r--r--components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java334
-rw-r--r--components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java287
-rw-r--r--components/cronet/android/test/src/org/chromium/net/Http2TestServer.java179
-rw-r--r--components/cronet/android/test/src/org/chromium/net/QuicTestServer.java16
-rw-r--r--components/cronet/cronet_static.gypi11
-rwxr-xr-xcomponents/cronet/tools/cr_cronet.py10
20 files changed, 3052 insertions, 86 deletions
diff --git a/components/cronet.gypi b/components/cronet.gypi
index 7194ca4..1dd1a64 100644
--- a/components/cronet.gypi
+++ b/components/cronet.gypi
@@ -3,6 +3,9 @@
# found in the LICENSE file.
{
+ 'variables': {
+ 'enable_bidirectional_stream%': 0,
+ },
'conditions': [
['OS=="android"', {
'targets': [
@@ -10,6 +13,7 @@
'target_name': 'cronet_jni_headers',
'type': 'none',
'sources': [
+ 'cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java',
'cronet/android/java/src/org/chromium/net/CronetLibraryLoader.java',
'cronet/android/java/src/org/chromium/net/CronetUploadDataStream.java',
'cronet/android/java/src/org/chromium/net/CronetUrlRequest.java',
@@ -193,6 +197,17 @@
'includes': [ 'cronet/cronet_static.gypi' ],
},
{
+ # GN version: //cronet:features
+ 'target_name': 'cronet_features',
+ 'includes': [ '../build/buildflag_header.gypi' ],
+ 'variables': {
+ 'buildflag_header_path': 'components/cronet/cronet_features.h',
+ 'buildflag_flags': [
+ 'ENABLE_BIDIRECTIONAL_STREAM=<(enable_bidirectional_stream)',
+ ],
+ },
+ },
+ {
'target_name': 'libcronet',
'type': 'shared_library',
'sources': [
@@ -242,6 +257,7 @@
'**/ChromiumUrlRequestError.java',
'**/ChromiumUrlRequestFactory.java',
'**/ChromiumUrlRequestPriority.java',
+ '**/CronetBidirectionalStream.java',
'**/CronetLibraryLoader.java',
'**/CronetUploadDataStream.java',
'**/CronetUrlRequest.java',
@@ -424,6 +440,16 @@
'is_test_apk': 1,
'run_findbugs': 1,
},
+ 'conditions': [
+ ['enable_bidirectional_stream==0', {
+ 'variables' : {
+ 'jar_excluded_classes': [
+ '**/BidirectionalStreamTest*',
+ '**/TestBidirectionalStreamCallback*',
+ ],
+ },
+ },],
+ ],
'includes': [ '../build/java_apk.gypi' ],
},
{
diff --git a/components/cronet/android/BUILD.gn b/components/cronet/android/BUILD.gn
index 24af5be..ed917a7 100644
--- a/components/cronet/android/BUILD.gn
+++ b/components/cronet/android/BUILD.gn
@@ -2,6 +2,7 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import("//build/buildflag_header.gni")
import("//build/config/android/config.gni")
import("//build/config/android/rules.gni")
import("//build/util/version.gni")
@@ -12,12 +13,14 @@ assert(!is_component_build, "Cronet requires static library build.")
declare_args() {
cronet_enable_data_reduction_proxy_support = false
+ cronet_enable_bidirectional_stream = false
}
generate_jni("cronet_jni_headers") {
sources = [
"java/src/org/chromium/net/ChromiumUrlRequest.java",
"java/src/org/chromium/net/ChromiumUrlRequestContext.java",
+ "java/src/org/chromium/net/CronetBidirectionalStream.java",
"java/src/org/chromium/net/CronetLibraryLoader.java",
"java/src/org/chromium/net/CronetUploadDataStream.java",
"java/src/org/chromium/net/CronetUrlRequest.java",
@@ -101,6 +104,7 @@ template("cronet_static_tmpl") {
":cronet_jni_headers",
":cronet_url_request_java",
":cronet_version_header",
+ ":features",
"//base",
"//base:i18n",
"//base:prefs",
@@ -156,6 +160,13 @@ template("cronet_static_tmpl") {
]
}
+ if (cronet_enable_bidirectional_stream) {
+ sources += [
+ "//components/cronet/android/cronet_bidirectional_stream_adapter.cc",
+ "//components/cronet/android/cronet_bidirectional_stream_adapter.h",
+ ]
+ }
+
if (defined(invoker.defines)) {
defines += invoker.defines
}
@@ -239,6 +250,7 @@ android_library("cronet_java") {
"java/src/org/chromium/net/ChromiumUrlRequest.java",
"java/src/org/chromium/net/ChromiumUrlRequestContext.java",
"java/src/org/chromium/net/ChromiumUrlRequestFactory.java",
+ "java/src/org/chromium/net/CronetBidirectionalStream.java",
"java/src/org/chromium/net/CronetLibraryLoader.java",
"java/src/org/chromium/net/CronetUploadDataStream.java",
"java/src/org/chromium/net/CronetUrlRequest.java",
@@ -401,7 +413,20 @@ android_resources("cronet_test_apk_resources") {
android_library("cronet_test_apk_java") {
testonly = true
- DEPRECATED_java_in_dir = "test/src"
+
+ java_files = [
+ "test/src/org/chromium/net/CronetTestApplication.java",
+ "test/src/org/chromium/net/MockCertVerifier.java",
+ "test/src/org/chromium/net/SdchObserver.java",
+ "test/src/org/chromium/net/CronetTestFramework.java",
+ "test/src/org/chromium/net/MockUrlRequestJobFactory.java",
+ "test/src/org/chromium/net/TestFilesInstaller.java",
+ "test/src/org/chromium/net/CronetTestUtil.java",
+ "test/src/org/chromium/net/NativeTestServer.java",
+ "test/src/org/chromium/net/TestUploadDataStreamHandler.java",
+ "test/src/org/chromium/net/NetworkChangeNotifierUtil.java",
+ "test/src/org/chromium/net/QuicTestServer.java",
+ ]
deps = [
":cronet_api",
@@ -409,6 +434,18 @@ android_library("cronet_test_apk_java") {
"//base:base_java",
"//net/android:net_java_test_support",
]
+
+ if (cronet_enable_bidirectional_stream) {
+ java_files += [
+ "test/src/org/chromium/net/Http2TestHandler.java",
+ "test/src/org/chromium/net/Http2TestServer.java",
+ ]
+
+ deps += [
+ "//third_party/netty-tcnative:netty-tcnative",
+ "//third_party/netty4:netty_all",
+ ]
+ }
}
android_assets("cronet_test_apk_assets") {
@@ -461,7 +498,6 @@ android_apk("cronet_test_apk") {
":cronet_tests",
"//base:base_java",
"//third_party/netty-tcnative:netty-tcnative_all",
- "//third_party/netty4:netty_all",
]
run_findbugs = true
@@ -471,7 +507,38 @@ instrumentation_test_apk("cronet_test_instrumentation_apk") {
apk_name = "CronetTestInstrumentation"
apk_under_test = ":cronet_test_apk"
android_manifest = "test/javatests/AndroidManifest.xml"
- DEPRECATED_java_in_dir = "test/javatests/src"
+
+ java_files = [
+ "test/javatests/src/org/chromium/net/ChromiumUrlRequestTest.java",
+ "test/javatests/src/org/chromium/net/ChunkedWritableByteChannelTest.java",
+ "test/javatests/src/org/chromium/net/ContextInitTest.java",
+ "test/javatests/src/org/chromium/net/Criteria.java",
+ "test/javatests/src/org/chromium/net/CronetTestBase.java",
+ "test/javatests/src/org/chromium/net/CronetUploadTest.java",
+ "test/javatests/src/org/chromium/net/CronetUrlRequestContextTest.java",
+ "test/javatests/src/org/chromium/net/CronetUrlRequestTest.java",
+ "test/javatests/src/org/chromium/net/CronetUrlTest.java",
+ "test/javatests/src/org/chromium/net/GetStatusTest.java",
+ "test/javatests/src/org/chromium/net/HttpUrlRequestFactoryTest.java",
+ "test/javatests/src/org/chromium/net/NetworkChangeNotifierTest.java",
+ "test/javatests/src/org/chromium/net/PkpTest.java",
+ "test/javatests/src/org/chromium/net/QuicTest.java",
+ "test/javatests/src/org/chromium/net/SdchTest.java",
+ "test/javatests/src/org/chromium/net/TestDrivenDataProvider.java",
+ "test/javatests/src/org/chromium/net/TestHttpUrlRequestListener.java",
+ "test/javatests/src/org/chromium/net/TestUploadDataProvider.java",
+ "test/javatests/src/org/chromium/net/TestUrlRequestCallback.java",
+ "test/javatests/src/org/chromium/net/UploadTest.java",
+ "test/javatests/src/org/chromium/net/UrlResponseInfoTest.java",
+ "test/javatests/src/org/chromium/net/urlconnection/CronetBufferedOutputStreamTest.java",
+ "test/javatests/src/org/chromium/net/urlconnection/CronetChunkedOutputStreamTest.java",
+ "test/javatests/src/org/chromium/net/urlconnection/CronetFixedModeOutputStreamTest.java",
+ "test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLConnectionTest.java",
+ "test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLStreamHandlerTest.java",
+ "test/javatests/src/org/chromium/net/urlconnection/CronetURLStreamHandlerFactoryTest.java",
+ "test/javatests/src/org/chromium/net/urlconnection/MessageLoopTest.java",
+ "test/javatests/src/org/chromium/net/urlconnection/TestUtil.java",
+ ]
deps = [
":cronet_api",
@@ -484,6 +551,13 @@ instrumentation_test_apk("cronet_test_instrumentation_apk") {
]
run_findbugs = true
+
+ if (cronet_enable_bidirectional_stream) {
+ java_files += [
+ "test/javatests/src/org/chromium/net/BidirectionalStreamTest.java",
+ "test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java",
+ ]
+ }
}
test("cronet_unittests") {
@@ -686,3 +760,9 @@ group("cronet_package") {
":repackage_extracted_jars",
]
}
+
+buildflag_header("features") {
+ header = "../cronet_features.h"
+
+ flags = [ "ENABLE_BIDIRECTIONAL_STREAM=$cronet_enable_bidirectional_stream" ]
+}
diff --git a/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java b/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java
index af42976..3c5f19c 100644
--- a/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java
+++ b/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java
@@ -88,7 +88,7 @@ public abstract class BidirectionalStream {
*/
public Builder setHttpMethod(String method) {
if (method == null) {
- throw new NullPointerException("Invalid method name.");
+ throw new NullPointerException("Method is required.");
}
mHttpMethod = method;
return this;
diff --git a/components/cronet/android/api/src/org/chromium/net/CronetException.java b/components/cronet/android/api/src/org/chromium/net/CronetException.java
index 9af6feee..9a9e3c4 100644
--- a/components/cronet/android/api/src/org/chromium/net/CronetException.java
+++ b/components/cronet/android/api/src/org/chromium/net/CronetException.java
@@ -14,6 +14,6 @@ public class CronetException extends UrlRequestException {
}
CronetException(String message, int netError) {
- super(message, null);
+ super(message, netError);
}
}
diff --git a/components/cronet/android/cronet_bidirectional_stream_adapter.cc b/components/cronet/android/cronet_bidirectional_stream_adapter.cc
new file mode 100644
index 0000000..27221ee
--- /dev/null
+++ b/components/cronet/android/cronet_bidirectional_stream_adapter.cc
@@ -0,0 +1,349 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "cronet_bidirectional_stream_adapter.h"
+
+#include <string>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/strings/string_number_conversions.h"
+#include "components/cronet/android/cronet_url_request_context_adapter.h"
+#include "jni/CronetBidirectionalStream_jni.h"
+#include "net/base/io_buffer.h"
+#include "net/base/net_errors.h"
+#include "net/base/request_priority.h"
+#include "net/http/bidirectional_stream_request_info.h"
+#include "net/http/http_network_session.h"
+#include "net/http/http_response_headers.h"
+#include "net/http/http_status_code.h"
+#include "net/http/http_transaction_factory.h"
+#include "net/http/http_util.h"
+#include "net/spdy/spdy_header_block.h"
+#include "net/ssl/ssl_info.h"
+#include "net/url_request/url_request_context.h"
+#include "url/gurl.h"
+
+using base::android::ConvertUTF8ToJavaString;
+using base::android::ConvertJavaStringToUTF8;
+
+namespace cronet {
+
+static jlong CreateBidirectionalStream(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& jbidi_stream,
+ jlong jurl_request_context_adapter) {
+ CronetURLRequestContextAdapter* context_adapter =
+ reinterpret_cast<CronetURLRequestContextAdapter*>(
+ jurl_request_context_adapter);
+ DCHECK(context_adapter);
+
+ CronetBidirectionalStreamAdapter* adapter =
+ new CronetBidirectionalStreamAdapter(context_adapter, env, jbidi_stream);
+
+ return reinterpret_cast<jlong>(adapter);
+}
+
+// TODO(mef): Extract this and its original from cronet_url_request_adapter.cc
+// into separate module.
+// net::WrappedIOBuffer subclass for a buffer owned by a Java ByteBuffer. Keeps
+// the ByteBuffer alive until destroyed. Uses WrappedIOBuffer because data() is
+// owned by the embedder.
+class CronetBidirectionalStreamAdapter::IOBufferWithByteBuffer
+ : public net::WrappedIOBuffer {
+ public:
+ // Creates a buffer wrapping the Java ByteBuffer |jbyte_buffer|.
+ // |byte_buffer_data| points to the memory backed by the ByteBuffer, and
+ // |position| is the index of the first byte of data inside of the buffer.
+ // |limit| is the the index of the first element that should not be read or
+ // written, preserved to verify that buffer is not changed externally during
+ // networking operations.
+ IOBufferWithByteBuffer(JNIEnv* env,
+ const JavaParamRef<jobject>& jbyte_buffer,
+ void* byte_buffer_data,
+ int position,
+ int limit)
+ : net::WrappedIOBuffer(static_cast<char*>(byte_buffer_data) + position),
+ byte_buffer_(env, jbyte_buffer),
+ initial_position_(position),
+ initial_limit_(limit) {
+ DCHECK(byte_buffer_data);
+ DCHECK_EQ(env->GetDirectBufferAddress(jbyte_buffer), byte_buffer_data);
+ }
+
+ int initial_position() const { return initial_position_; }
+ int initial_limit() const { return initial_limit_; }
+
+ jobject byte_buffer() const { return byte_buffer_.obj(); }
+
+ private:
+ ~IOBufferWithByteBuffer() override {}
+
+ base::android::ScopedJavaGlobalRef<jobject> byte_buffer_;
+
+ const int initial_position_;
+ const int initial_limit_;
+};
+
+// static
+bool CronetBidirectionalStreamAdapter::RegisterJni(JNIEnv* env) {
+ return RegisterNativesImpl(env);
+}
+
+CronetBidirectionalStreamAdapter::CronetBidirectionalStreamAdapter(
+ CronetURLRequestContextAdapter* context,
+ JNIEnv* env,
+ const JavaParamRef<jobject>& jbidi_stream)
+ : context_(context), owner_(env, jbidi_stream) {}
+
+CronetBidirectionalStreamAdapter::~CronetBidirectionalStreamAdapter() {
+ DCHECK(context_->IsOnNetworkThread());
+}
+
+jint CronetBidirectionalStreamAdapter::Start(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& jcaller,
+ const JavaParamRef<jstring>& jurl,
+ jint jpriority,
+ const JavaParamRef<jstring>& jmethod,
+ const JavaParamRef<jobjectArray>& jheaders,
+ jboolean jend_of_stream) {
+ // Prepare request info here to be able to return the error.
+ scoped_ptr<net::BidirectionalStreamRequestInfo> request_info(
+ new net::BidirectionalStreamRequestInfo());
+ request_info->url = GURL(ConvertJavaStringToUTF8(env, jurl));
+ request_info->priority = static_cast<net::RequestPriority>(jpriority);
+ // Http method is a token, just as header name.
+ request_info->method = ConvertJavaStringToUTF8(env, jmethod);
+ if (!net::HttpUtil::IsValidHeaderName(request_info->method))
+ return -1;
+
+ std::vector<std::string> headers;
+ base::android::AppendJavaStringArrayToStringVector(env, jheaders, &headers);
+ for (size_t i = 0; i < headers.size(); i += 2) {
+ std::string name(headers[i]);
+ std::string value(headers[i + 1]);
+ if (!net::HttpUtil::IsValidHeaderName(name) ||
+ !net::HttpUtil::IsValidHeaderValue(value)) {
+ return i + 1;
+ }
+ request_info->extra_headers.SetHeader(name, value);
+ }
+ request_info->end_stream_on_headers = jend_of_stream;
+
+ context_->PostTaskToNetworkThread(
+ FROM_HERE,
+ base::Bind(&CronetBidirectionalStreamAdapter::StartOnNetworkThread,
+ base::Unretained(this), base::Passed(&request_info)));
+ return 0;
+}
+
+jboolean CronetBidirectionalStreamAdapter::ReadData(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& jcaller,
+ const JavaParamRef<jobject>& jbyte_buffer,
+ jint jposition,
+ jint jlimit) {
+ DCHECK_LT(jposition, jlimit);
+
+ void* data = env->GetDirectBufferAddress(jbyte_buffer);
+ if (!data)
+ return JNI_FALSE;
+
+ scoped_refptr<IOBufferWithByteBuffer> read_buffer(
+ new IOBufferWithByteBuffer(env, jbyte_buffer, data, jposition, jlimit));
+
+ int remaining_capacity = jlimit - jposition;
+
+ context_->PostTaskToNetworkThread(
+ FROM_HERE,
+ base::Bind(&CronetBidirectionalStreamAdapter::ReadDataOnNetworkThread,
+ base::Unretained(this), read_buffer, remaining_capacity));
+ return JNI_TRUE;
+}
+
+jboolean CronetBidirectionalStreamAdapter::WriteData(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& jcaller,
+ const JavaParamRef<jobject>& jbyte_buffer,
+ jint jposition,
+ jint jlimit,
+ jboolean jend_of_stream) {
+ DCHECK_LE(jposition, jlimit);
+
+ void* data = env->GetDirectBufferAddress(jbyte_buffer);
+ if (!data)
+ return JNI_FALSE;
+
+ scoped_refptr<IOBufferWithByteBuffer> write_buffer(
+ new IOBufferWithByteBuffer(env, jbyte_buffer, data, jposition, jlimit));
+
+ int remaining_capacity = jlimit - jposition;
+
+ context_->PostTaskToNetworkThread(
+ FROM_HERE,
+ base::Bind(&CronetBidirectionalStreamAdapter::WriteDataOnNetworkThread,
+ base::Unretained(this), write_buffer, remaining_capacity,
+ jend_of_stream));
+ return JNI_TRUE;
+}
+
+void CronetBidirectionalStreamAdapter::Destroy(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& jcaller,
+ jboolean jsend_on_canceled) {
+ // Destroy could be called from any thread, including network thread (if
+ // posting task to executor throws an exception), but is posted, so |this|
+ // is valid until calling task is complete. Destroy() is always called from
+ // within a synchronized java block that guarantees no future posts to the
+ // network thread with the adapter pointer.
+ context_->PostTaskToNetworkThread(
+ FROM_HERE,
+ base::Bind(&CronetBidirectionalStreamAdapter::DestroyOnNetworkThread,
+ base::Unretained(this), jsend_on_canceled));
+}
+
+void CronetBidirectionalStreamAdapter::OnHeadersSent() {
+ DCHECK(context_->IsOnNetworkThread());
+ JNIEnv* env = base::android::AttachCurrentThread();
+ cronet::Java_CronetBidirectionalStream_onRequestHeadersSent(env,
+ owner_.obj());
+}
+
+void CronetBidirectionalStreamAdapter::OnHeadersReceived(
+ const net::SpdyHeaderBlock& response_headers) {
+ DCHECK(context_->IsOnNetworkThread());
+ JNIEnv* env = base::android::AttachCurrentThread();
+ // Get http status code from response headers.
+ jint http_status_code = 0;
+ const auto http_status_header = response_headers.find(":status");
+ if (http_status_header != response_headers.end())
+ base::StringToInt(http_status_header->second, &http_status_code);
+
+ std::string protocol;
+ switch (bidi_stream_->GetProtocol()) {
+ case net::kProtoHTTP2:
+ protocol = "h2";
+ default:
+ break;
+ }
+
+ cronet::Java_CronetBidirectionalStream_onResponseHeadersReceived(
+ env, owner_.obj(), http_status_code,
+ ConvertUTF8ToJavaString(env, protocol).obj(),
+ GetHeadersArray(env, response_headers).obj(),
+ bidi_stream_->GetTotalReceivedBytes());
+}
+
+void CronetBidirectionalStreamAdapter::OnDataRead(int bytes_read) {
+ DCHECK(context_->IsOnNetworkThread());
+ JNIEnv* env = base::android::AttachCurrentThread();
+ cronet::Java_CronetBidirectionalStream_onReadCompleted(
+ env, owner_.obj(), read_buffer_->byte_buffer(), bytes_read,
+ read_buffer_->initial_position(), read_buffer_->initial_limit(),
+ bidi_stream_->GetTotalReceivedBytes());
+ // Free the read buffer. This lets the Java ByteBuffer be freed, if the
+ // embedder releases it, too.
+ read_buffer_ = nullptr;
+}
+
+void CronetBidirectionalStreamAdapter::OnDataSent() {
+ DCHECK(context_->IsOnNetworkThread());
+ JNIEnv* env = base::android::AttachCurrentThread();
+ cronet::Java_CronetBidirectionalStream_onWriteCompleted(
+ env, owner_.obj(), write_buffer_->byte_buffer(),
+ write_buffer_->initial_position(), write_buffer_->initial_limit());
+ // Free the write buffer. This lets the Java ByteBuffer be freed, if the
+ // embedder releases it, too.
+ write_buffer_ = nullptr;
+}
+
+void CronetBidirectionalStreamAdapter::OnTrailersReceived(
+ const net::SpdyHeaderBlock& response_trailers) {
+ DCHECK(context_->IsOnNetworkThread());
+ JNIEnv* env = base::android::AttachCurrentThread();
+ cronet::Java_CronetBidirectionalStream_onResponseTrailersReceived(
+ env, owner_.obj(), GetHeadersArray(env, response_trailers).obj());
+}
+
+void CronetBidirectionalStreamAdapter::OnFailed(int error) {
+ DCHECK(context_->IsOnNetworkThread());
+ JNIEnv* env = base::android::AttachCurrentThread();
+ cronet::Java_CronetBidirectionalStream_onError(
+ env, owner_.obj(), error,
+ ConvertUTF8ToJavaString(env, net::ErrorToString(error)).obj(),
+ bidi_stream_->GetTotalReceivedBytes());
+}
+
+void CronetBidirectionalStreamAdapter::StartOnNetworkThread(
+ scoped_ptr<net::BidirectionalStreamRequestInfo> request_info) {
+ DCHECK(context_->IsOnNetworkThread());
+ DCHECK(!bidi_stream_);
+ bidi_stream_.reset(new net::BidirectionalStream(
+ std::move(request_info), context_->GetURLRequestContext()
+ ->http_transaction_factory()
+ ->GetSession(),
+ this));
+}
+
+void CronetBidirectionalStreamAdapter::ReadDataOnNetworkThread(
+ scoped_refptr<IOBufferWithByteBuffer> read_buffer,
+ int buffer_size) {
+ DCHECK(context_->IsOnNetworkThread());
+ DCHECK(read_buffer);
+ DCHECK(!read_buffer_);
+
+ read_buffer_ = read_buffer;
+
+ int bytes_read = bidi_stream_->ReadData(read_buffer_.get(), buffer_size);
+ // If IO is pending, wait for the BidirectionalStream to call OnDataRead.
+ if (bytes_read == net::ERR_IO_PENDING)
+ return;
+
+ if (bytes_read < 0) {
+ OnFailed(bytes_read);
+ return;
+ }
+ OnDataRead(bytes_read);
+}
+
+void CronetBidirectionalStreamAdapter::WriteDataOnNetworkThread(
+ scoped_refptr<IOBufferWithByteBuffer> write_buffer,
+ int buffer_size,
+ bool end_of_stream) {
+ DCHECK(context_->IsOnNetworkThread());
+ DCHECK(write_buffer);
+ DCHECK(!write_buffer_);
+
+ write_buffer_ = write_buffer;
+ bidi_stream_->SendData(write_buffer_.get(), buffer_size, end_of_stream);
+}
+
+void CronetBidirectionalStreamAdapter::DestroyOnNetworkThread(
+ bool send_on_canceled) {
+ DCHECK(context_->IsOnNetworkThread());
+ if (send_on_canceled) {
+ JNIEnv* env = base::android::AttachCurrentThread();
+ cronet::Java_CronetBidirectionalStream_onCanceled(env, owner_.obj());
+ }
+ delete this;
+}
+
+base::android::ScopedJavaLocalRef<jobjectArray>
+CronetBidirectionalStreamAdapter::GetHeadersArray(
+ JNIEnv* env,
+ const net::SpdyHeaderBlock& header_block) {
+ DCHECK(context_->IsOnNetworkThread());
+
+ std::vector<std::string> headers;
+ for (const auto& header : header_block) {
+ headers.push_back(header.first.as_string());
+ headers.push_back(header.second.as_string());
+ }
+ return base::android::ToJavaArrayOfStrings(env, headers);
+}
+
+} // namespace cronet
diff --git a/components/cronet/android/cronet_bidirectional_stream_adapter.h b/components/cronet/android/cronet_bidirectional_stream_adapter.h
new file mode 100644
index 0000000..14b403e
--- /dev/null
+++ b/components/cronet/android/cronet_bidirectional_stream_adapter.h
@@ -0,0 +1,130 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_CRONET_ANDROID_CRONET_BIDIRECTIONAL_STREAM_ADAPTER_H_
+#define COMPONENTS_CRONET_ANDROID_CRONET_BIDIRECTIONAL_STREAM_ADAPTER_H_
+
+#include <jni.h>
+#include <string>
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_array.h"
+#include "base/android/jni_string.h"
+#include "base/android/scoped_java_ref.h"
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "net/http/bidirectional_stream.h"
+
+namespace net {
+class BidirectionalStreamRequestInfo;
+class SpdyHeaderBlock;
+} // namespace net
+
+namespace cronet {
+
+class CronetURLRequestContextAdapter;
+
+// An adapter from Java BidirectionalStream object to net::BidirectionalStream.
+// Created and configured from a Java thread. Start, ReadData, WriteData and
+// Destroy can be called on any thread (including network thread), and post
+// calls to corresponding {Start|ReadData|WriteData|Destroy}OnNetworkThread to
+// the network thread. The object is always deleted on network thread. All
+// callbacks into the Java BidirectionalStream are done on the network thread.
+// Java BidirectionalStream is expected to initiate the next step like ReadData
+// or Destroy. Public methods can be called on any thread.
+class CronetBidirectionalStreamAdapter
+ : public net::BidirectionalStream::Delegate {
+ public:
+ static bool RegisterJni(JNIEnv* env);
+
+ CronetBidirectionalStreamAdapter(
+ CronetURLRequestContextAdapter* context,
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& jbidi_stream);
+ ~CronetBidirectionalStreamAdapter() override;
+
+ // Validates method and headers, initializes and starts the request. If
+ // |jend_of_stream| is true, then stream is half-closed after sending header
+ // frame and no data is expected to be written.
+ // Returns 0 if request is valid and started successfully,
+ // Returns -1 if |jmethod| is not valid HTTP method name.
+ // Returns position of invalid header value in |jheaders| if header name is
+ // not valid.
+ jint Start(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& jcaller,
+ const base::android::JavaParamRef<jstring>& jurl,
+ jint jpriority,
+ const base::android::JavaParamRef<jstring>& jmethod,
+ const base::android::JavaParamRef<jobjectArray>& jheaders,
+ jboolean jend_of_stream);
+
+ // Reads more data into |jbyte_buffer| starting at |jposition| and not
+ // exceeding |jlimit|. Arguments are preserved to ensure that |jbyte_buffer|
+ // is not modified by the application during read.
+ jboolean ReadData(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& jcaller,
+ const base::android::JavaParamRef<jobject>& jbyte_buffer,
+ jint jposition,
+ jint jlimit);
+
+ // Writes more data from |jbyte_buffer| starting at |jposition| and ending at
+ // |jlimit|-1. Arguments are preserved to ensure that |jbyte_buffer|
+ // is not modified by the application during write. The |jend_of_stream| is
+ // passed to remote to indicate end of stream.
+ jboolean WriteData(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& jcaller,
+ const base::android::JavaParamRef<jobject>& jbyte_buffer,
+ jint jposition,
+ jint jlimit,
+ jboolean jend_of_stream);
+
+ // Releases all resources for the request and deletes the object itself.
+ // |jsend_on_canceled| indicates if Java onCanceled callback should be
+ // issued to indicate that no more callbacks will be issued.
+ void Destroy(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& jcaller,
+ jboolean jsend_on_canceled);
+
+ private:
+ class IOBufferWithByteBuffer;
+
+ // net::BidirectionalStream::Delegate implementations:
+ void OnHeadersSent() override;
+ void OnHeadersReceived(const net::SpdyHeaderBlock& response_headers) override;
+ void OnDataRead(int bytes_read) override;
+ void OnDataSent() override;
+ void OnTrailersReceived(const net::SpdyHeaderBlock& trailers) override;
+ void OnFailed(int error) override;
+
+ void StartOnNetworkThread(
+ scoped_ptr<net::BidirectionalStreamRequestInfo> request_info);
+ void ReadDataOnNetworkThread(
+ scoped_refptr<IOBufferWithByteBuffer> read_buffer,
+ int buffer_size);
+ void WriteDataOnNetworkThread(
+ scoped_refptr<IOBufferWithByteBuffer> read_buffer,
+ int buffer_size,
+ bool end_of_stream);
+ void DestroyOnNetworkThread(bool send_on_canceled);
+ // Gets headers as a Java array.
+ base::android::ScopedJavaLocalRef<jobjectArray> GetHeadersArray(
+ JNIEnv* env,
+ const net::SpdyHeaderBlock& header_block);
+
+ CronetURLRequestContextAdapter* const context_;
+
+ // Java object that owns this CronetBidirectionalStreamAdapter.
+ base::android::ScopedJavaGlobalRef<jobject> owner_;
+
+ scoped_refptr<IOBufferWithByteBuffer> read_buffer_;
+ scoped_refptr<IOBufferWithByteBuffer> write_buffer_;
+ scoped_ptr<net::BidirectionalStream> bidi_stream_;
+
+ DISALLOW_COPY_AND_ASSIGN(CronetBidirectionalStreamAdapter);
+};
+
+} // namespace cronet
+
+#endif // COMPONENTS_CRONET_ANDROID_CRONET_BIDIRECTIONAL_STREAM_ADAPTER_H_
diff --git a/components/cronet/android/cronet_library_loader.cc b/components/cronet/android/cronet_library_loader.cc
index 018d000..85b8ebf 100644
--- a/components/cronet/android/cronet_library_loader.cc
+++ b/components/cronet/android/cronet_library_loader.cc
@@ -21,6 +21,7 @@
#include "components/cronet/android/cronet_upload_data_stream_adapter.h"
#include "components/cronet/android/cronet_url_request_adapter.h"
#include "components/cronet/android/cronet_url_request_context_adapter.h"
+#include "components/cronet/cronet_features.h"
#include "components/cronet/version.h"
#include "jni/CronetLibraryLoader_jni.h"
#include "net/android/net_jni_registrar.h"
@@ -28,6 +29,10 @@
#include "net/base/network_change_notifier.h"
#include "url/url_util.h"
+#if BUILDFLAG(ENABLE_BIDIRECTIONAL_STREAM)
+#include "components/cronet/android/cronet_bidirectional_stream_adapter.h"
+#endif
+
#if defined(USE_ICU_ALTERNATIVES_ON_ANDROID)
#include "url/android/url_jni_registrar.h" // nogncheck
#else
@@ -41,6 +46,10 @@ const base::android::RegistrationMethod kCronetRegisteredMethods[] = {
{"BaseAndroid", base::android::RegisterJni},
{"ChromiumUrlRequest", ChromiumUrlRequestRegisterJni},
{"ChromiumUrlRequestContext", ChromiumUrlRequestContextRegisterJni},
+#if BUILDFLAG(ENABLE_BIDIRECTIONAL_STREAM)
+ {"CronetBidirectionalStreamAdapter",
+ CronetBidirectionalStreamAdapter::RegisterJni},
+#endif
{"CronetLibraryLoader", RegisterNativesImpl},
{"CronetUploadDataStreamAdapter", CronetUploadDataStreamAdapterRegisterJni},
{"CronetUrlRequestAdapter", CronetUrlRequestAdapterRegisterJni},
diff --git a/components/cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java b/components/cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java
new file mode 100644
index 0000000..53fdea1
--- /dev/null
+++ b/components/cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java
@@ -0,0 +1,644 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.net;
+
+import org.chromium.base.Log;
+import org.chromium.base.VisibleForTesting;
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+import org.chromium.base.annotations.NativeClassQualifiedName;
+
+import java.nio.ByteBuffer;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * {@link BidirectionalStream} implementation using Chromium network stack.
+ * All @CalledByNative methods are called on the native network thread
+ * and post tasks with callback calls onto Executor. Upon returning from callback, the native
+ * stream is called on Executor thread and posts native tasks to the native network thread.
+ */
+@JNINamespace("cronet")
+class CronetBidirectionalStream extends BidirectionalStream {
+ /**
+ * States of BidirectionalStream are tracked in mReadState and mWriteState.
+ * The write state is separated out as it changes independently of the read state.
+ * There is one initial state: State.NOT_STARTED. There is one normal final state:
+ * State.SUCCESS, reached after State.READING_DONE and State.WRITING_DONE. There are two
+ * exceptional final states: State.CANCELED and State.ERROR, which can be reached from
+ * any other non-final state.
+ */
+ private enum State {
+ /* Initial state, stream not started. */
+ NOT_STARTED,
+ /* Stream started, request headers are being sent. */
+ STARTED,
+ /* Waiting for {@code read()} to be called. */
+ WAITING_FOR_READ,
+ /* Reading from the remote, {@code onReadCompleted()} callback will be called when done. */
+ READING,
+ /* There is no more data to read and stream is half-closed by the remote side. */
+ READING_DONE,
+ /* Stream is canceled. */
+ CANCELED,
+ /* Error has occured, stream is closed. */
+ ERROR,
+ /* Reading and writing are done, and the stream is closed successfully. */
+ SUCCESS,
+ /* Waiting for {@code write()} to be called. */
+ WAITING_FOR_WRITE,
+ /* Writing to the remote, {@code onWriteCompleted()} callback will be called when done. */
+ WRITING,
+ /* There is no more data to write and stream is half-closed by the local side. */
+ WRITING_DONE,
+ }
+
+ private final CronetUrlRequestContext mRequestContext;
+ private final Executor mExecutor;
+ private final Callback mCallback;
+ private final String mInitialUrl;
+ private final int mInitialPriority;
+ private final String mInitialMethod;
+ private final String mRequestHeaders[];
+
+ /*
+ * Synchronizes access to mNativeStream, mReadState and mWriteState.
+ */
+ private final Object mNativeStreamLock = new Object();
+
+ /* Native BidirectionalStream object, owned by CronetBidirectionalStream. */
+ @GuardedBy("mNativeStreamLock")
+ private long mNativeStream;
+
+ /**
+ * Read state is tracking reading flow.
+ * / <--- READING <--- \
+ * | |
+ * \ /
+ * NOT_STARTED -> STARTED --> WAITING_FOR_READ -> READING_DONE -> SUCCESS
+ */
+ @GuardedBy("mNativeStreamLock")
+ private State mReadState = State.NOT_STARTED;
+
+ /**
+ * Write state is tracking writing flow.
+ * / <--- WRITING <--- \
+ * | |
+ * \ /
+ * NOT_STARTED -> STARTED --> WAITING_FOR_WRITE -> WRITING_DONE -> SUCCESS
+ */
+ @GuardedBy("mNativeStreamLock")
+ private State mWriteState = State.NOT_STARTED;
+
+ private UrlResponseInfo mResponseInfo;
+
+ /*
+ * OnReadCompleted callback is repeatedly invoked when each read is completed, so it
+ * is cached as a member variable.
+ */
+ private OnReadCompletedRunnable mOnReadCompletedTask;
+
+ /*
+ * OnWriteCompleted callback is repeatedly invoked when each write is completed, so it
+ * is cached as a member variable.
+ */
+ private OnWriteCompletedRunnable mOnWriteCompletedTask;
+
+ private Runnable mOnDestroyedCallbackForTesting;
+
+ private final class OnReadCompletedRunnable implements Runnable {
+ // Buffer passed back from current invocation of onReadCompleted.
+ ByteBuffer mByteBuffer;
+ // End of stream flag from current invocation of onReadCompleted.
+ boolean mEndOfStream;
+
+ @Override
+ public void run() {
+ try {
+ // Null out mByteBuffer, to pass buffer ownership to callback or release if done.
+ ByteBuffer buffer = mByteBuffer;
+ mByteBuffer = null;
+ synchronized (mNativeStreamLock) {
+ if (isDoneLocked()) {
+ return;
+ }
+ if (mEndOfStream) {
+ mReadState = State.READING_DONE;
+ if (maybeSucceedLocked()) {
+ return;
+ }
+ } else {
+ mReadState = State.WAITING_FOR_READ;
+ }
+ }
+ mCallback.onReadCompleted(CronetBidirectionalStream.this, mResponseInfo, buffer);
+ } catch (Exception e) {
+ onCallbackException(e);
+ }
+ }
+ }
+
+ private final class OnWriteCompletedRunnable implements Runnable {
+ // Buffer passed back from current invocation of onWriteCompleted.
+ ByteBuffer mByteBuffer;
+ // End of stream flag from current call to write.
+ boolean mEndOfStream;
+
+ @Override
+ public void run() {
+ try {
+ // Null out mByteBuffer, to pass buffer ownership to callback or release if done.
+ ByteBuffer buffer = mByteBuffer;
+ mByteBuffer = null;
+ synchronized (mNativeStreamLock) {
+ if (isDoneLocked()) {
+ return;
+ }
+ if (mEndOfStream) {
+ mWriteState = State.WRITING_DONE;
+ if (maybeSucceedLocked()) {
+ return;
+ }
+ } else {
+ mWriteState = State.WAITING_FOR_WRITE;
+ }
+ }
+ mCallback.onWriteCompleted(CronetBidirectionalStream.this, mResponseInfo, buffer);
+ } catch (Exception e) {
+ onCallbackException(e);
+ }
+ }
+ }
+
+ CronetBidirectionalStream(CronetUrlRequestContext requestContext, String url,
+ @BidirectionalStream.Builder.StreamPriority int priority, Callback callback,
+ Executor executor, String httpMethod, List<Map.Entry<String, String>> requestHeaders) {
+ mRequestContext = requestContext;
+ mInitialUrl = url;
+ mInitialPriority = convertStreamPriority(priority);
+ mCallback = callback;
+ mExecutor = executor;
+ mInitialMethod = httpMethod;
+ mRequestHeaders = stringsFromHeaderList(requestHeaders);
+ }
+
+ @Override
+ public void start() {
+ synchronized (mNativeStreamLock) {
+ if (mReadState != State.NOT_STARTED) {
+ throw new IllegalStateException("Stream is already started.");
+ }
+ try {
+ mNativeStream = nativeCreateBidirectionalStream(
+ mRequestContext.getUrlRequestContextAdapter());
+ mRequestContext.onRequestStarted();
+ // Non-zero startResult means an argument error.
+ int startResult = nativeStart(mNativeStream, mInitialUrl, mInitialPriority,
+ mInitialMethod, mRequestHeaders, !doesMethodAllowWriteData(mInitialMethod));
+ if (startResult == -1) {
+ throw new IllegalArgumentException("Invalid http method " + mInitialMethod);
+ }
+ if (startResult > 0) {
+ int headerPos = startResult - 1;
+ throw new IllegalArgumentException("Invalid header "
+ + mRequestHeaders[headerPos] + "=" + mRequestHeaders[headerPos + 1]);
+ }
+ mReadState = mWriteState = State.STARTED;
+ } catch (RuntimeException e) {
+ // If there's an exception, clean up and then throw the
+ // exception to the caller.
+ destroyNativeStreamLocked(false);
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public void read(ByteBuffer buffer) {
+ synchronized (mNativeStreamLock) {
+ Preconditions.checkHasRemaining(buffer);
+ Preconditions.checkDirect(buffer);
+ if (mReadState != State.WAITING_FOR_READ) {
+ throw new IllegalStateException("Unexpected read attempt.");
+ }
+ if (isDoneLocked()) {
+ return;
+ }
+ if (mOnReadCompletedTask == null) {
+ mOnReadCompletedTask = new OnReadCompletedRunnable();
+ }
+ mReadState = State.READING;
+ if (!nativeReadData(mNativeStream, buffer, buffer.position(), buffer.limit())) {
+ // Still waiting on read. This is just to have consistent
+ // behavior with the other error cases.
+ mReadState = State.WAITING_FOR_READ;
+ throw new IllegalArgumentException("Unable to call native read");
+ }
+ }
+ }
+
+ @Override
+ public void write(ByteBuffer buffer, boolean endOfStream) {
+ synchronized (mNativeStreamLock) {
+ Preconditions.checkDirect(buffer);
+ if (!buffer.hasRemaining() && !endOfStream) {
+ throw new IllegalArgumentException("Empty buffer before end of stream.");
+ }
+ if (mWriteState != State.WAITING_FOR_WRITE) {
+ throw new IllegalStateException("Unexpected write attempt.");
+ }
+ if (isDoneLocked()) {
+ return;
+ }
+ if (mOnWriteCompletedTask == null) {
+ mOnWriteCompletedTask = new OnWriteCompletedRunnable();
+ }
+ mOnWriteCompletedTask.mEndOfStream = endOfStream;
+ mWriteState = State.WRITING;
+ if (!nativeWriteData(
+ mNativeStream, buffer, buffer.position(), buffer.limit(), endOfStream)) {
+ // Still waiting on write. This is just to have consistent
+ // behavior with the other error cases.
+ mWriteState = State.WAITING_FOR_WRITE;
+ throw new IllegalArgumentException("Unable to call native write");
+ }
+ }
+ }
+
+ @Override
+ public void ping(PingCallback callback, Executor executor) {
+ // TODO(mef): May be last thing to be implemented on Android.
+ throw new UnsupportedOperationException("ping is not supported yet.");
+ }
+
+ @Override
+ public void windowUpdate(int windowSizeIncrement) {
+ // TODO(mef): Understand the needs and semantics of this method.
+ throw new UnsupportedOperationException("windowUpdate is not supported yet.");
+ }
+
+ @Override
+ public void cancel() {
+ synchronized (mNativeStreamLock) {
+ if (isDoneLocked() || mReadState == State.NOT_STARTED) {
+ return;
+ }
+ mReadState = mWriteState = State.CANCELED;
+ destroyNativeStreamLocked(true);
+ }
+ }
+
+ @Override
+ public boolean isDone() {
+ synchronized (mNativeStreamLock) {
+ return isDoneLocked();
+ }
+ }
+
+ @GuardedBy("mNativeStreamLock")
+ private boolean isDoneLocked() {
+ return mReadState != State.NOT_STARTED && mNativeStream == 0;
+ }
+
+ @SuppressWarnings("unused")
+ @CalledByNative
+ private void onRequestHeadersSent() {
+ postTaskToExecutor(new Runnable() {
+ public void run() {
+ synchronized (mNativeStreamLock) {
+ if (isDoneLocked()) {
+ return;
+ }
+ if (doesMethodAllowWriteData(mInitialMethod)) {
+ mWriteState = State.WAITING_FOR_WRITE;
+ } else {
+ mWriteState = State.WRITING_DONE;
+ }
+ }
+
+ try {
+ mCallback.onRequestHeadersSent(CronetBidirectionalStream.this);
+ } catch (Exception e) {
+ onCallbackException(e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Called when the final set of headers, after all redirects,
+ * is received. Can only be called once for each stream.
+ */
+ @SuppressWarnings("unused")
+ @CalledByNative
+ private void onResponseHeadersReceived(int httpStatusCode, String negotiatedProtocol,
+ String[] headers, long receivedBytesCount) {
+ try {
+ mResponseInfo = prepareResponseInfoOnNetworkThread(
+ httpStatusCode, negotiatedProtocol, headers, receivedBytesCount);
+ } catch (Exception e) {
+ failWithException(new CronetException("Cannot prepare ResponseInfo", null));
+ return;
+ }
+ postTaskToExecutor(new Runnable() {
+ public void run() {
+ synchronized (mNativeStreamLock) {
+ if (isDoneLocked()) {
+ return;
+ }
+ mReadState = State.WAITING_FOR_READ;
+ }
+
+ try {
+ mCallback.onResponseHeadersReceived(
+ CronetBidirectionalStream.this, mResponseInfo);
+ } catch (Exception e) {
+ onCallbackException(e);
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("unused")
+ @CalledByNative
+ private void onReadCompleted(final ByteBuffer byteBuffer, int bytesRead, int initialPosition,
+ int initialLimit, long receivedBytesCount) {
+ mResponseInfo.setReceivedBytesCount(receivedBytesCount);
+ if (byteBuffer.position() != initialPosition || byteBuffer.limit() != initialLimit) {
+ failWithException(
+ new CronetException("ByteBuffer modified externally during read", null));
+ return;
+ }
+ if (bytesRead < 0 || initialPosition + bytesRead > initialLimit) {
+ failWithException(new CronetException("Invalid number of bytes read", null));
+ return;
+ }
+ byteBuffer.position(initialPosition + bytesRead);
+ assert mOnReadCompletedTask.mByteBuffer == null;
+ mOnReadCompletedTask.mByteBuffer = byteBuffer;
+ mOnReadCompletedTask.mEndOfStream = (bytesRead == 0);
+ postTaskToExecutor(mOnReadCompletedTask);
+ }
+
+ @SuppressWarnings("unused")
+ @CalledByNative
+ private void onWriteCompleted(
+ final ByteBuffer byteBuffer, int initialPosition, int initialLimit) {
+ if (byteBuffer.position() != initialPosition || byteBuffer.limit() != initialLimit) {
+ failWithException(
+ new CronetException("ByteBuffer modified externally during write", null));
+ return;
+ }
+ // Current implementation always writes the complete buffer.
+ byteBuffer.position(byteBuffer.limit());
+ assert mOnWriteCompletedTask.mByteBuffer == null;
+ mOnWriteCompletedTask.mByteBuffer = byteBuffer;
+ postTaskToExecutor(mOnWriteCompletedTask);
+ }
+
+ @SuppressWarnings("unused")
+ @CalledByNative
+ private void onResponseTrailersReceived(String[] trailers) {
+ final UrlResponseInfo.HeaderBlock trailersBlock =
+ new UrlResponseInfo.HeaderBlock(headersListFromStrings(trailers));
+ postTaskToExecutor(new Runnable() {
+ public void run() {
+ synchronized (mNativeStreamLock) {
+ if (isDoneLocked()) {
+ return;
+ }
+ }
+ try {
+ mCallback.onResponseTrailersReceived(
+ CronetBidirectionalStream.this, mResponseInfo, trailersBlock);
+ } catch (Exception e) {
+ onCallbackException(e);
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("unused")
+ @CalledByNative
+ private void onError(final int nativeError, final String errorString, long receivedBytesCount) {
+ if (mResponseInfo != null) {
+ mResponseInfo.setReceivedBytesCount(receivedBytesCount);
+ }
+ failWithException(new CronetException(
+ "Exception in BidirectionalStream: " + errorString, nativeError));
+ }
+
+ /**
+ * Called when request is canceled, no callbacks will be called afterwards.
+ */
+ @SuppressWarnings("unused")
+ @CalledByNative
+ private void onCanceled() {
+ postTaskToExecutor(new Runnable() {
+ public void run() {
+ try {
+ mCallback.onCanceled(CronetBidirectionalStream.this, mResponseInfo);
+ } catch (Exception e) {
+ Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onCanceled method", e);
+ }
+ }
+ });
+ }
+
+ @VisibleForTesting
+ public void setOnDestroyedCallbackForTesting(Runnable onDestroyedCallbackForTesting) {
+ mOnDestroyedCallbackForTesting = onDestroyedCallbackForTesting;
+ }
+
+ private static boolean doesMethodAllowWriteData(String methodName) {
+ return !methodName.equals("GET") && !methodName.equals("HEAD");
+ }
+
+ private static ArrayList<Map.Entry<String, String>> headersListFromStrings(String[] headers) {
+ ArrayList<Map.Entry<String, String>> headersList = new ArrayList<>(headers.length / 2);
+ for (int i = 0; i < headers.length; i += 2) {
+ headersList.add(new AbstractMap.SimpleImmutableEntry<>(headers[i], headers[i + 1]));
+ }
+ return headersList;
+ }
+
+ private static String[] stringsFromHeaderList(List<Map.Entry<String, String>> headersList) {
+ String headersArray[] = new String[headersList.size() * 2];
+ int i = 0;
+ for (Map.Entry<String, String> requestHeader : headersList) {
+ headersArray[i++] = requestHeader.getKey();
+ headersArray[i++] = requestHeader.getValue();
+ }
+ return headersArray;
+ }
+
+ private static int convertStreamPriority(
+ @BidirectionalStream.Builder.StreamPriority int priority) {
+ switch (priority) {
+ case Builder.STREAM_PRIORITY_IDLE:
+ return RequestPriority.IDLE;
+ case Builder.STREAM_PRIORITY_LOWEST:
+ return RequestPriority.LOWEST;
+ case Builder.STREAM_PRIORITY_LOW:
+ return RequestPriority.LOW;
+ case Builder.STREAM_PRIORITY_MEDIUM:
+ return RequestPriority.MEDIUM;
+ case Builder.STREAM_PRIORITY_HIGHEST:
+ return RequestPriority.HIGHEST;
+ default:
+ throw new IllegalArgumentException("Invalid stream priority.");
+ }
+ }
+
+ /**
+ * Checks whether reading and writing are done.
+ * @return false if either reading or writing is not done. If both reading and writing
+ * are done, then posts cleanup task and returns true.
+ */
+ @GuardedBy("mNativeStreamLock")
+ private boolean maybeSucceedLocked() {
+ if (mReadState != State.READING_DONE || mWriteState != State.WRITING_DONE) {
+ return false;
+ }
+
+ mReadState = mWriteState = State.SUCCESS;
+ postTaskToExecutor(new Runnable() {
+ public void run() {
+ synchronized (mNativeStreamLock) {
+ if (isDoneLocked()) {
+ return;
+ }
+ // Destroy native stream first, so UrlRequestContext could be shut
+ // down from the listener.
+ destroyNativeStreamLocked(false);
+ }
+ try {
+ mCallback.onSucceeded(CronetBidirectionalStream.this, mResponseInfo);
+ } catch (Exception e) {
+ Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onSucceeded method", e);
+ }
+ }
+ });
+ return true;
+ }
+
+ /**
+ * Posts task to application Executor. Used for callbacks
+ * and other tasks that should not be executed on network thread.
+ */
+ private void postTaskToExecutor(Runnable task) {
+ try {
+ mExecutor.execute(task);
+ } catch (RejectedExecutionException failException) {
+ Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to executor",
+ failException);
+ // If posting a task throws an exception, then there is no choice
+ // but to destroy the stream without invoking the callback.
+ synchronized (mNativeStreamLock) {
+ mReadState = mWriteState = State.ERROR;
+ destroyNativeStreamLocked(false);
+ }
+ }
+ }
+
+ private UrlResponseInfo prepareResponseInfoOnNetworkThread(int httpStatusCode,
+ String negotiatedProtocol, String[] headers, long receivedBytesCount) {
+ synchronized (mNativeStreamLock) {
+ if (mNativeStream == 0) {
+ return null;
+ }
+ }
+
+ ArrayList<String> urlChain = new ArrayList<>();
+ urlChain.add(mInitialUrl);
+
+ UrlResponseInfo responseInfo = new UrlResponseInfo(urlChain, httpStatusCode, "",
+ headersListFromStrings(headers), false, negotiatedProtocol, null);
+
+ responseInfo.setReceivedBytesCount(receivedBytesCount);
+ return responseInfo;
+ }
+
+ @GuardedBy("mNativeStreamLock")
+ private void destroyNativeStreamLocked(boolean sendOnCanceled) {
+ Log.i(CronetUrlRequestContext.LOG_TAG, "destroyNativeStreamLocked " + this.toString());
+ if (mNativeStream == 0) {
+ return;
+ }
+ nativeDestroy(mNativeStream, sendOnCanceled);
+ mNativeStream = 0;
+ mRequestContext.onRequestDestroyed();
+ if (mOnDestroyedCallbackForTesting != null) {
+ mOnDestroyedCallbackForTesting.run();
+ }
+ }
+
+ /**
+ * Fails the stream with an exception. Only called on the Executor.
+ */
+ private void failWithExceptionOnExecutor(CronetException e) {
+ // Do not call into listener if request is complete.
+ synchronized (mNativeStreamLock) {
+ if (isDoneLocked()) {
+ return;
+ }
+ mReadState = mWriteState = State.ERROR;
+ destroyNativeStreamLocked(false);
+ }
+ try {
+ mCallback.onFailed(this, mResponseInfo, e);
+ } catch (Exception failException) {
+ Log.e(CronetUrlRequestContext.LOG_TAG, "Exception notifying of failed request",
+ failException);
+ }
+ }
+
+ /**
+ * If callback method throws an exception, stream gets canceled
+ * and exception is reported via onFailed callback.
+ * Only called on the Executor.
+ */
+ private void onCallbackException(Exception e) {
+ CronetException streamError =
+ new CronetException("CalledByNative method has thrown an exception", e);
+ Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in CalledByNative method", e);
+ failWithExceptionOnExecutor(streamError);
+ }
+
+ /**
+ * Fails the stream with an exception. Can be called on any thread.
+ */
+ private void failWithException(final CronetException exception) {
+ postTaskToExecutor(new Runnable() {
+ public void run() {
+ failWithExceptionOnExecutor(exception);
+ }
+ });
+ }
+
+ // Native methods are implemented in cronet_bidirectional_stream_adapter.cc.
+ private native long nativeCreateBidirectionalStream(long urlRequestContextAdapter);
+
+ @NativeClassQualifiedName("CronetBidirectionalStreamAdapter")
+ private native int nativeStart(long nativePtr, String url, int priority, String method,
+ String[] headers, boolean endOfStream);
+
+ @NativeClassQualifiedName("CronetBidirectionalStreamAdapter")
+ private native boolean nativeReadData(
+ long nativePtr, ByteBuffer byteBuffer, int position, int limit);
+
+ @NativeClassQualifiedName("CronetBidirectionalStreamAdapter")
+ private native boolean nativeWriteData(
+ long nativePtr, ByteBuffer byteBuffer, int position, int limit, boolean endOfStream);
+
+ @NativeClassQualifiedName("CronetBidirectionalStreamAdapter")
+ private native void nativeDestroy(long nativePtr, boolean sendOnCanceled);
+}
diff --git a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java
index a618f1b..75c61a5 100644
--- a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java
+++ b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java
@@ -194,7 +194,7 @@ final class CronetUrlRequest implements UrlRequest {
try {
mUrlRequestAdapter = nativeCreateRequestAdapter(
mRequestContext.getUrlRequestContextAdapter(), mInitialUrl, mPriority);
- mRequestContext.onRequestStarted(this);
+ mRequestContext.onRequestStarted();
if (mInitialMethod != null) {
if (!nativeSetHttpMethod(mUrlRequestAdapter, mInitialMethod)) {
throw new IllegalArgumentException("Invalid http method " + mInitialMethod);
@@ -439,7 +439,7 @@ final class CronetUrlRequest implements UrlRequest {
}
nativeDestroy(mUrlRequestAdapter, sendOnCanceled);
mRequestContext.reportFinished(this);
- mRequestContext.onRequestDestroyed(this);
+ mRequestContext.onRequestDestroyed();
mUrlRequestAdapter = 0;
if (mOnDestroyedCallbackForTesting != null) {
mOnDestroyedCallbackForTesting.run();
diff --git a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequestContext.java b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequestContext.java
index cac2e32..cf6dd24 100644
--- a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequestContext.java
+++ b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequestContext.java
@@ -143,7 +143,11 @@ class CronetUrlRequestContext extends CronetEngine {
BidirectionalStream createBidirectionalStream(String url, BidirectionalStream.Callback callback,
Executor executor, String httpMethod, List<Map.Entry<String, String>> requestHeaders,
@BidirectionalStream.Builder.StreamPriority int priority) {
- throw new UnsupportedOperationException();
+ synchronized (mLock) {
+ checkHaveAdapter();
+ return new CronetBidirectionalStream(
+ this, url, priority, callback, executor, httpMethod, requestHeaders);
+ }
}
@Override
@@ -343,7 +347,7 @@ class CronetUrlRequestContext extends CronetEngine {
* Mark request as started to prevent shutdown when there are active
* requests.
*/
- void onRequestStarted(UrlRequest urlRequest) {
+ void onRequestStarted() {
mActiveRequestCount.incrementAndGet();
}
@@ -351,7 +355,7 @@ class CronetUrlRequestContext extends CronetEngine {
* Mark request as finished to allow shutdown when there are no active
* requests.
*/
- void onRequestDestroyed(UrlRequest urlRequest) {
+ void onRequestDestroyed() {
mActiveRequestCount.decrementAndGet();
}
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java
index 7e10f83..3c85b78 100644
--- a/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java
@@ -4,15 +4,22 @@
package org.chromium.net;
+import android.os.ConditionVariable;
import android.test.suitebuilder.annotation.SmallTest;
import org.chromium.base.test.util.Feature;
+import org.chromium.net.CronetTestBase.OnlyRunNativeCronet;
+import org.chromium.net.TestBidirectionalStreamCallback.FailureType;
+import org.chromium.net.TestBidirectionalStreamCallback.ResponseStep;
import java.nio.ByteBuffer;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* Test functionality of BidirectionalStream interface.
@@ -23,63 +30,56 @@ public class BidirectionalStreamTest extends CronetTestBase {
@Override
protected void setUp() throws Exception {
super.setUp();
- mTestFramework = startCronetTestFramework();
- assertTrue(NativeTestServer.startNativeTestServer(getContext()));
- // Add url interceptors after native application context is initialized.
- MockUrlRequestJobFactory.setUp();
+ // Load library first to create MockCertVerifier.
+ System.loadLibrary("cronet_tests");
+ CronetEngine.Builder builder = new CronetEngine.Builder(getContext());
+ builder.setMockCertVerifierForTesting(QuicTestServer.createMockCertVerifier());
+
+ mTestFramework = startCronetTestFrameworkWithUrlAndCronetEngineBuilder(null, builder);
+ assertTrue(Http2TestServer.startHttp2TestServer(
+ getContext(), QuicTestServer.getServerCert(), QuicTestServer.getServerCertKey()));
}
@Override
protected void tearDown() throws Exception {
- NativeTestServer.shutdownNativeTestServer();
- mTestFramework.mCronetEngine.shutdown();
+ assertTrue(Http2TestServer.shutdownHttp2TestServer());
+ if (mTestFramework.mCronetEngine != null) {
+ mTestFramework.mCronetEngine.shutdown();
+ }
super.tearDown();
}
- private class TestBidirectionalStreamCallback extends BidirectionalStream.Callback {
- // Executor Service for Cronet callbacks.
- private final ExecutorService mExecutorService =
- Executors.newSingleThreadExecutor(new ExecutorThreadFactory());
- private Thread mExecutorThread;
+ private static void checkResponseInfo(UrlResponseInfo responseInfo, String expectedUrl,
+ int expectedHttpStatusCode, String expectedHttpStatusText) {
+ assertEquals(expectedUrl, responseInfo.getUrl());
+ assertEquals(
+ expectedUrl, responseInfo.getUrlChain().get(responseInfo.getUrlChain().size() - 1));
+ assertEquals(expectedHttpStatusCode, responseInfo.getHttpStatusCode());
+ assertEquals(expectedHttpStatusText, responseInfo.getHttpStatusText());
+ assertFalse(responseInfo.wasCached());
+ assertTrue(responseInfo.toString().length() > 0);
+ }
- private class ExecutorThreadFactory implements ThreadFactory {
- public Thread newThread(Runnable r) {
- mExecutorThread = new Thread(r);
- return mExecutorThread;
- }
+ private static String createLongString(String base, int repetition) {
+ StringBuilder builder = new StringBuilder(base.length() * repetition);
+ for (int i = 0; i < repetition; ++i) {
+ builder.append(i);
+ builder.append(base);
}
+ return builder.toString();
+ }
- @Override
- public void onRequestHeadersSent(BidirectionalStream stream) {}
-
- @Override
- public void onResponseHeadersReceived(BidirectionalStream stream, UrlResponseInfo info) {}
-
- @Override
- public void onReadCompleted(
- BidirectionalStream stream, UrlResponseInfo info, ByteBuffer buffer) {}
-
- @Override
- public void onWriteCompleted(
- BidirectionalStream stream, UrlResponseInfo info, ByteBuffer buffer) {}
-
- @Override
- public void onResponseTrailersReceived(BidirectionalStream stream, UrlResponseInfo info,
- UrlResponseInfo.HeaderBlock trailers) {}
-
- @Override
- public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) {}
-
- @Override
- public void onFailed(
- BidirectionalStream stream, UrlResponseInfo info, CronetException error) {}
-
- @Override
- public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) {}
-
- Executor getExecutor() {
- return mExecutorService;
+ private static UrlResponseInfo createUrlResponseInfo(
+ String[] urls, String message, int statusCode, int receivedBytes, String... headers) {
+ ArrayList<Map.Entry<String, String>> headersList = new ArrayList<>();
+ for (int i = 0; i < headers.length; i += 2) {
+ headersList.add(new AbstractMap.SimpleImmutableEntry<String, String>(
+ headers[i], headers[i + 1]));
}
+ UrlResponseInfo urlResponseInfo = new UrlResponseInfo(
+ Arrays.asList(urls), statusCode, message, headersList, false, "h2", null);
+ urlResponseInfo.setReceivedBytesCount(receivedBytes);
+ return urlResponseInfo;
}
@SmallTest
@@ -94,28 +94,918 @@ public class BidirectionalStreamTest extends CronetTestBase {
assertEquals("URL is required.", e.getMessage());
}
try {
- new BidirectionalStream.Builder(NativeTestServer.getRedirectURL(), null,
+ new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), null,
callback.getExecutor(), mTestFramework.mCronetEngine);
fail("Callback not null-checked");
} catch (NullPointerException e) {
assertEquals("Callback is required.", e.getMessage());
}
try {
- new BidirectionalStream.Builder(NativeTestServer.getRedirectURL(), callback, null,
- mTestFramework.mCronetEngine);
+ new BidirectionalStream.Builder(
+ Http2TestServer.getServerUrl(), callback, null, mTestFramework.mCronetEngine);
fail("Executor not null-checked");
} catch (NullPointerException e) {
assertEquals("Executor is required.", e.getMessage());
}
try {
new BidirectionalStream.Builder(
- NativeTestServer.getRedirectURL(), callback, callback.getExecutor(), null);
+ Http2TestServer.getServerUrl(), callback, callback.getExecutor(), null);
fail("CronetEngine not null-checked");
} catch (NullPointerException e) {
assertEquals("CronetEngine is required.", e.getMessage());
}
// Verify successful creation doesn't throw.
- new BidirectionalStream.Builder(NativeTestServer.getRedirectURL(), callback,
- callback.getExecutor(), mTestFramework.mCronetEngine);
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ try {
+ builder.addHeader(null, "value");
+ fail("Header name is not null-checked");
+ } catch (NullPointerException e) {
+ assertEquals("Invalid header name.", e.getMessage());
+ }
+ try {
+ builder.addHeader("name", null);
+ fail("Header value is not null-checked");
+ } catch (NullPointerException e) {
+ assertEquals("Invalid header value.", e.getMessage());
+ }
+ try {
+ builder.setHttpMethod(null);
+ fail("Method name is not null-checked");
+ } catch (NullPointerException e) {
+ assertEquals("Method is required.", e.getMessage());
+ }
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testFailPlainHttp() throws Exception {
+ String url = "http://example.com";
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .build();
+ stream.start();
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals("Exception in BidirectionalStream: net::ERR_DISALLOWED_URL_SCHEME",
+ callback.mError.getMessage());
+ assertEquals(-301, callback.mError.netError());
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testSimpleGet() throws Exception {
+ String url = Http2TestServer.getEchoMethodUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .setHttpMethod("GET")
+ .build();
+ stream.start();
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ // Default method is 'GET'.
+ assertEquals("GET", callback.mResponseAsString);
+ UrlResponseInfo urlResponseInfo =
+ createUrlResponseInfo(new String[] {url}, "", 200, 27, ":status", "200");
+ assertResponseEquals(urlResponseInfo, callback.mResponseInfo);
+ checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, "");
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testSimpleHead() throws Exception {
+ String url = Http2TestServer.getEchoMethodUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .setHttpMethod("HEAD")
+ .build();
+ stream.start();
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals("HEAD", callback.mResponseAsString);
+ UrlResponseInfo urlResponseInfo =
+ createUrlResponseInfo(new String[] {url}, "", 200, 28, ":status", "200");
+ assertResponseEquals(urlResponseInfo, callback.mResponseInfo);
+ checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, "");
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testSimplePost() throws Exception {
+ String url = Http2TestServer.getEchoStreamUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.addWriteData("Test String".getBytes());
+ callback.addWriteData("1234567890".getBytes());
+ callback.addWriteData("woot!".getBytes());
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .addHeader("foo", "bar")
+ .addHeader("empty", "")
+ .addHeader("Content-Type", "zebra")
+ .build();
+ stream.start();
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals("Test String1234567890woot!", callback.mResponseAsString);
+ assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
+ assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0));
+ assertEquals(
+ "zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testSimplePut() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.addWriteData("Put This Data!".getBytes());
+ String methodName = "PUT";
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ builder.setHttpMethod(methodName);
+ builder.build().start();
+ callback.blockForDone();
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals("Put This Data!", callback.mResponseAsString);
+ assertEquals(methodName, callback.mResponseInfo.getAllHeaders().get("echo-method").get(0));
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testBadMethod() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ try {
+ builder.setHttpMethod("bad:method!");
+ builder.build().start();
+ fail("IllegalArgumentException not thrown.");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Invalid http method bad:method!", e.getMessage());
+ }
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testBadHeaderName() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ try {
+ builder.addHeader("goodheader1", "headervalue");
+ builder.addHeader("header:name", "headervalue");
+ builder.addHeader("goodheader2", "headervalue");
+ builder.build().start();
+ fail("IllegalArgumentException not thrown.");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Invalid header header:name=headervalue", e.getMessage());
+ }
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testBadHeaderValue() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ try {
+ builder.addHeader("headername", "bad header\r\nvalue");
+ builder.build().start();
+ fail("IllegalArgumentException not thrown.");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Invalid header headername=bad header\r\nvalue", e.getMessage());
+ }
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testAddHeader() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ String headerName = "header-name";
+ String headerValue = "header-value";
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoHeaderUrl(headerName),
+ callback, callback.getExecutor(), mTestFramework.mCronetEngine);
+ builder.addHeader(headerName, headerValue);
+ builder.setHttpMethod("GET");
+ builder.build().start();
+ callback.blockForDone();
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals(headerValue, callback.mResponseAsString);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testMultiRequestHeaders() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ String headerName = "header-name";
+ String headerValue1 = "header-value1";
+ String headerValue2 = "header-value2";
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoAllHeadersUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ builder.addHeader(headerName, headerValue1);
+ builder.addHeader(headerName, headerValue2);
+ builder.setHttpMethod("GET");
+ builder.build().start();
+ callback.blockForDone();
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ String headers = callback.mResponseAsString;
+ Pattern pattern = Pattern.compile(headerName + ":\\s(.*)\\r\\n");
+ Matcher matcher = pattern.matcher(headers);
+ List<String> actualValues = new ArrayList<String>();
+ while (matcher.find()) {
+ actualValues.add(matcher.group(1));
+ }
+ assertEquals(1, actualValues.size());
+ assertEquals("header-value2", actualValues.get(0));
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testEchoTrailers() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ String headerName = "header-name";
+ String headerValue = "header-value";
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoTrailersUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ builder.addHeader(headerName, headerValue);
+ builder.setHttpMethod("GET");
+ builder.build().start();
+ callback.blockForDone();
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertNotNull(callback.mTrailers);
+ // Verify that header value is properly echoed in trailers.
+ assertEquals(headerValue, callback.mTrailers.getAsMap().get("echo-" + headerName).get(0));
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testCustomUserAgent() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ String userAgentName = "User-Agent";
+ String userAgentValue = "User-Agent-Value";
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoHeaderUrl(userAgentName),
+ callback, callback.getExecutor(), mTestFramework.mCronetEngine);
+ builder.setHttpMethod("GET");
+ builder.addHeader(userAgentName, userAgentValue);
+ builder.build().start();
+ callback.blockForDone();
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals(userAgentValue, callback.mResponseAsString);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testEchoStream() throws Exception {
+ String url = Http2TestServer.getEchoStreamUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ String[] testData = {"Test String", createLongString("1234567890", 50000), "woot!"};
+ StringBuilder stringData = new StringBuilder();
+ for (String writeData : testData) {
+ callback.addWriteData(writeData.getBytes());
+ stringData.append(writeData);
+ }
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .addHeader("foo", "Value with Spaces")
+ .addHeader("Content-Type", "zebra")
+ .build();
+ stream.start();
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals(stringData.toString(), callback.mResponseAsString);
+ assertEquals(
+ "Value with Spaces", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
+ assertEquals(
+ "zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testEchoStreamEmptyWrite() throws Exception {
+ String url = Http2TestServer.getEchoStreamUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.addWriteData(new byte[0]);
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .build();
+ stream.start();
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals("", callback.mResponseAsString);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testDoubleWrite() throws Exception {
+ String url = Http2TestServer.getEchoStreamUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() {
+ @Override
+ public void onRequestHeadersSent(BidirectionalStream stream) {
+ startNextWrite(stream);
+ try {
+ // Second write from callback invoked on single-threaded executor throws
+ // an exception because first write is still pending until its completion
+ // is handled on executor.
+ ByteBuffer writeBuffer = ByteBuffer.allocateDirect(5);
+ writeBuffer.put("abc".getBytes());
+ writeBuffer.flip();
+ stream.write(writeBuffer, false);
+ fail("Exception is not thrown.");
+ } catch (Exception e) {
+ assertEquals("Unexpected write attempt.", e.getMessage());
+ }
+ }
+ };
+ callback.addWriteData("1".getBytes());
+ callback.addWriteData("2".getBytes());
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .build();
+ stream.start();
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals("12", callback.mResponseAsString);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testDoubleRead() throws Exception {
+ String url = Http2TestServer.getEchoStreamUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() {
+ @Override
+ public void onResponseHeadersReceived(
+ BidirectionalStream stream, UrlResponseInfo info) {
+ startNextRead(stream);
+ try {
+ // Second read from callback invoked on single-threaded executor throws
+ // an exception because previous read is still pending until its completion
+ // is handled on executor.
+ stream.read(ByteBuffer.allocateDirect(5));
+ fail("Exception is not thrown.");
+ } catch (Exception e) {
+ assertEquals("Unexpected read attempt.", e.getMessage());
+ }
+ }
+ };
+ callback.addWriteData("1".getBytes());
+ callback.addWriteData("2".getBytes());
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .build();
+ stream.start();
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals("12", callback.mResponseAsString);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testReadAndWrite() throws Exception {
+ String url = Http2TestServer.getEchoStreamUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() {
+ @Override
+ public void onResponseHeadersReceived(
+ BidirectionalStream stream, UrlResponseInfo info) {
+ // Start the write, that will not complete until callback completion.
+ startNextWrite(stream);
+ // Start the read. It is allowed with write in flight.
+ super.onResponseHeadersReceived(stream, info);
+ }
+ };
+ callback.setAutoAdvance(false);
+ callback.addWriteData("1".getBytes());
+ callback.addWriteData("2".getBytes());
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .build();
+ stream.start();
+ callback.waitForNextWriteStep();
+ callback.waitForNextReadStep();
+ callback.startNextRead(stream);
+ callback.setAutoAdvance(true);
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals("12", callback.mResponseAsString);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testEchoStreamWriteFirst() throws Exception {
+ String url = Http2TestServer.getEchoStreamUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.setAutoAdvance(false);
+ String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woot!"};
+ StringBuilder stringData = new StringBuilder();
+ for (String writeData : testData) {
+ callback.addWriteData(writeData.getBytes());
+ stringData.append(writeData);
+ }
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .build();
+ stream.start();
+ // Write first.
+ callback.waitForNextWriteStep();
+ for (String expected : testData) {
+ // Write next chunk of test data.
+ callback.startNextWrite(stream);
+ callback.waitForNextWriteStep();
+ }
+
+ // Wait for read step, but don't read yet.
+ callback.waitForNextReadStep();
+ assertEquals("", callback.mResponseAsString);
+ // Read back.
+ callback.startNextRead(stream);
+ callback.waitForNextReadStep();
+ // Verify that some part of proper response is read.
+ assertTrue(callback.mResponseAsString.startsWith(testData[0]));
+ assertTrue(stringData.toString().startsWith(callback.mResponseAsString));
+ // Read the rest of the response.
+ callback.setAutoAdvance(true);
+ callback.startNextRead(stream);
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals(stringData.toString(), callback.mResponseAsString);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testEchoStreamStepByStep() throws Exception {
+ String url = Http2TestServer.getEchoStreamUrl();
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.setAutoAdvance(false);
+ String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woot!"};
+ StringBuilder stringData = new StringBuilder();
+ for (String writeData : testData) {
+ callback.addWriteData(writeData.getBytes());
+ stringData.append(writeData);
+ }
+ // Create stream.
+ BidirectionalStream stream = new BidirectionalStream
+ .Builder(url, callback, callback.getExecutor(),
+ mTestFramework.mCronetEngine)
+ .build();
+ stream.start();
+ callback.waitForNextWriteStep();
+ callback.waitForNextReadStep();
+
+ for (String expected : testData) {
+ // Write next chunk of test data.
+ callback.startNextWrite(stream);
+ callback.waitForNextWriteStep();
+
+ // Read next chunk of test data.
+ ByteBuffer readBuffer = ByteBuffer.allocateDirect(100);
+ callback.startNextRead(stream, readBuffer);
+ callback.waitForNextReadStep();
+ assertEquals(expected.length(), readBuffer.position());
+ assertFalse(stream.isDone());
+ }
+
+ callback.setAutoAdvance(true);
+ callback.startNextRead(stream);
+ callback.blockForDone();
+ assertTrue(stream.isDone());
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals(stringData.toString(), callback.mResponseAsString);
+ }
+
+ /**
+ * Checks that the buffer is updated correctly, when starting at an offset.
+ */
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testSimpleGetBufferUpdates() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.setAutoAdvance(false);
+ // Since the method is "GET", the expected response body is also "GET".
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ BidirectionalStream stream = builder.setHttpMethod("GET").build();
+ stream.start();
+ callback.waitForNextReadStep();
+
+ assertEquals(null, callback.mError);
+ assertFalse(callback.isDone());
+ assertEquals(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED,
+ callback.mResponseStep);
+
+ ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
+ readBuffer.put("FOR".getBytes());
+ assertEquals(3, readBuffer.position());
+
+ // Read first two characters of the response ("GE"). It's theoretically
+ // possible to need one read per character, though in practice,
+ // shouldn't happen.
+ while (callback.mResponseAsString.length() < 2) {
+ assertFalse(callback.isDone());
+ callback.startNextRead(stream, readBuffer);
+ callback.waitForNextReadStep();
+ }
+
+ // Make sure the two characters were read.
+ assertEquals("GE", callback.mResponseAsString);
+
+ // Check the contents of the entire buffer. The first 3 characters
+ // should not have been changed, and the last two should be the first
+ // two characters from the response.
+ assertEquals("FORGE", bufferContentsToString(readBuffer, 0, 5));
+ // The limit and position should be 5.
+ assertEquals(5, readBuffer.limit());
+ assertEquals(5, readBuffer.position());
+
+ assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep);
+
+ // Start reading from position 3. Since the only remaining character
+ // from the response is a "T", when the read completes, the buffer
+ // should contain "FORTE", with a position() of 4 and a limit() of 5.
+ readBuffer.position(3);
+ callback.startNextRead(stream, readBuffer);
+ callback.waitForNextReadStep();
+
+ // Make sure all three characters of the response have now been read.
+ assertEquals("GET", callback.mResponseAsString);
+
+ // Check the entire contents of the buffer. Only the third character
+ // should have been modified.
+ assertEquals("FORTE", bufferContentsToString(readBuffer, 0, 5));
+
+ // Make sure position and limit were updated correctly.
+ assertEquals(4, readBuffer.position());
+ assertEquals(5, readBuffer.limit());
+
+ assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep);
+
+ // One more read attempt. The request should complete.
+ readBuffer.position(1);
+ readBuffer.limit(5);
+ callback.startNextRead(stream, readBuffer);
+ callback.waitForNextReadStep();
+
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals("GET", callback.mResponseAsString);
+ checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, "");
+
+ // Check that buffer contents were not modified.
+ assertEquals("FORTE", bufferContentsToString(readBuffer, 0, 5));
+
+ // Position should not have been modified, since nothing was read.
+ assertEquals(1, readBuffer.position());
+ // Limit should be unchanged as always.
+ assertEquals(5, readBuffer.limit());
+
+ assertEquals(ResponseStep.ON_SUCCEEDED, callback.mResponseStep);
+
+ // Make sure there are no other pending messages, which would trigger
+ // asserts in TestBidirectionalCallback.
+ testSimpleGet();
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testBadBuffers() throws Exception {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.setAutoAdvance(false);
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ BidirectionalStream stream = builder.setHttpMethod("GET").build();
+ stream.start();
+ callback.waitForNextReadStep();
+
+ assertEquals(null, callback.mError);
+ assertFalse(callback.isDone());
+ assertEquals(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED,
+ callback.mResponseStep);
+
+ // Try to read using a full buffer.
+ try {
+ ByteBuffer readBuffer = ByteBuffer.allocateDirect(4);
+ readBuffer.put("full".getBytes());
+ stream.read(readBuffer);
+ fail("Exception not thrown");
+ } catch (IllegalArgumentException e) {
+ assertEquals("ByteBuffer is already full.", e.getMessage());
+ }
+
+ // Try to read using a non-direct buffer.
+ try {
+ ByteBuffer readBuffer = ByteBuffer.allocate(5);
+ stream.read(readBuffer);
+ fail("Exception not thrown");
+ } catch (Exception e) {
+ assertEquals("byteBuffer must be a direct ByteBuffer.", e.getMessage());
+ }
+
+ // Finish the stream with a direct ByteBuffer.
+ callback.setAutoAdvance(true);
+ ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
+ stream.read(readBuffer);
+ callback.blockForDone();
+ assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
+ assertEquals("GET", callback.mResponseAsString);
+ }
+
+ private void throwOrCancel(FailureType failureType, ResponseStep failureStep,
+ boolean expectResponseInfo, boolean expectError) {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.setFailure(failureType, failureStep);
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ BidirectionalStream stream = builder.setHttpMethod("GET").build();
+ stream.start();
+ callback.blockForDone();
+ // assertEquals(callback.mResponseStep, failureStep);
+ assertTrue(stream.isDone());
+ assertEquals(expectResponseInfo, callback.mResponseInfo != null);
+ assertEquals(expectError, callback.mError != null);
+ assertEquals(expectError, callback.mOnErrorCalled);
+ assertEquals(failureType == FailureType.CANCEL_SYNC
+ || failureType == FailureType.CANCEL_ASYNC
+ || failureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE,
+ callback.mOnCanceledCalled);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testFailures() throws Exception {
+ throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_REQUEST_HEADERS_SENT, false, false);
+ throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_REQUEST_HEADERS_SENT, false, false);
+ throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_REQUEST_HEADERS_SENT,
+ false, false);
+ throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_REQUEST_HEADERS_SENT, false, true);
+
+ throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_RESPONSE_STARTED, true, false);
+ throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_RESPONSE_STARTED, true, false);
+ throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_RESPONSE_STARTED,
+ true, false);
+ throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_RESPONSE_STARTED, true, true);
+
+ throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_READ_COMPLETED, true, false);
+ throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_READ_COMPLETED, true, false);
+ throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_READ_COMPLETED, true,
+ false);
+ throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED, true, true);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testThrowOnSucceeded() {
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_SUCCEEDED);
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ BidirectionalStream stream = builder.setHttpMethod("GET").build();
+ stream.start();
+ callback.blockForDone();
+ assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED);
+ assertTrue(stream.isDone());
+ assertNotNull(callback.mResponseInfo);
+ // Check that error thrown from 'onSucceeded' callback is not reported.
+ assertNull(callback.mError);
+ assertFalse(callback.mOnErrorCalled);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testExecutorShutdownBeforeStreamIsDone() {
+ // Test that stream is destroyed even if executor is shut down and rejects posting tasks.
+ TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
+ callback.setAutoAdvance(false);
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ CronetBidirectionalStream stream =
+ (CronetBidirectionalStream) builder.setHttpMethod("GET").build();
+ stream.start();
+ callback.waitForNextReadStep();
+ assertFalse(callback.isDone());
+ assertFalse(stream.isDone());
+
+ final ConditionVariable streamDestroyed = new ConditionVariable(false);
+ stream.setOnDestroyedCallbackForTesting(new Runnable() {
+ @Override
+ public void run() {
+ streamDestroyed.open();
+ }
+ });
+
+ // Shut down the executor, so posting the task will throw an exception.
+ callback.shutdownExecutor();
+ ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
+ stream.read(readBuffer);
+ // Callback will never be called again because executor is shut down,
+ // but stream will be destroyed from network thread.
+ streamDestroyed.block();
+
+ assertFalse(callback.isDone());
+ assertTrue(stream.isDone());
+ }
+
+ /**
+ * Callback that shuts down the engine when the stream has succeeded
+ * or failed.
+ */
+ private class ShutdownTestBidirectionalStreamCallback extends TestBidirectionalStreamCallback {
+ @Override
+ public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) {
+ mTestFramework.mCronetEngine.shutdown();
+ // Clear mCronetEngine so it doesn't get shut down second time in tearDown().
+ mTestFramework.mCronetEngine = null;
+ super.onSucceeded(stream, info);
+ }
+
+ @Override
+ public void onFailed(
+ BidirectionalStream stream, UrlResponseInfo info, CronetException error) {
+ mTestFramework.mCronetEngine.shutdown();
+ // Clear mCronetEngine so it doesn't get shut down second time in tearDown().
+ mTestFramework.mCronetEngine = null;
+ super.onFailed(stream, info, error);
+ }
+
+ @Override
+ public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) {
+ mTestFramework.mCronetEngine.shutdown();
+ // Clear mCronetEngine so it doesn't get shut down second time in tearDown().
+ mTestFramework.mCronetEngine = null;
+ super.onCanceled(stream, info);
+ }
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testCronetEngineShutdown() throws Exception {
+ // Test that CronetEngine cannot be shut down if there are any active streams.
+ TestBidirectionalStreamCallback callback = new ShutdownTestBidirectionalStreamCallback();
+ // Block callback when response starts to verify that shutdown fails
+ // if there are active streams.
+ callback.setAutoAdvance(false);
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ CronetBidirectionalStream stream =
+ (CronetBidirectionalStream) builder.setHttpMethod("GET").build();
+ stream.start();
+ try {
+ mTestFramework.mCronetEngine.shutdown();
+ fail("Should throw an exception");
+ } catch (Exception e) {
+ assertEquals("Cannot shutdown with active requests.", e.getMessage());
+ }
+
+ callback.waitForNextReadStep();
+ assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep);
+ try {
+ mTestFramework.mCronetEngine.shutdown();
+ fail("Should throw an exception");
+ } catch (Exception e) {
+ assertEquals("Cannot shutdown with active requests.", e.getMessage());
+ }
+ callback.startNextRead(stream);
+
+ callback.waitForNextReadStep();
+ assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep);
+ try {
+ mTestFramework.mCronetEngine.shutdown();
+ fail("Should throw an exception");
+ } catch (Exception e) {
+ assertEquals("Cannot shutdown with active requests.", e.getMessage());
+ }
+
+ // May not have read all the data, in theory. Just enable auto-advance
+ // and finish the request.
+ callback.setAutoAdvance(true);
+ callback.startNextRead(stream);
+ callback.blockForDone();
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testCronetEngineShutdownAfterStreamFailure() throws Exception {
+ // Test that CronetEngine can be shut down after stream reports a failure.
+ TestBidirectionalStreamCallback callback = new ShutdownTestBidirectionalStreamCallback();
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ CronetBidirectionalStream stream =
+ (CronetBidirectionalStream) builder.setHttpMethod("GET").build();
+ stream.start();
+ callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED);
+ callback.blockForDone();
+ assertTrue(callback.mOnErrorCalled);
+ assertNull(mTestFramework.mCronetEngine);
+ }
+
+ @SmallTest
+ @Feature({"Cronet"})
+ @OnlyRunNativeCronet
+ public void testCronetEngineShutdownAfterStreamCancel() throws Exception {
+ // Test that CronetEngine can be shut down after stream is canceled.
+ TestBidirectionalStreamCallback callback = new ShutdownTestBidirectionalStreamCallback();
+ BidirectionalStream.Builder builder =
+ new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback,
+ callback.getExecutor(), mTestFramework.mCronetEngine);
+ CronetBidirectionalStream stream =
+ (CronetBidirectionalStream) builder.setHttpMethod("GET").build();
+
+ // Block callback when response starts to verify that shutdown fails
+ // if there are active requests.
+ callback.setAutoAdvance(false);
+ stream.start();
+ try {
+ mTestFramework.mCronetEngine.shutdown();
+ fail("Should throw an exception");
+ } catch (Exception e) {
+ assertEquals("Cannot shutdown with active requests.", e.getMessage());
+ }
+ callback.waitForNextReadStep();
+ assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep);
+ stream.cancel();
+ callback.blockForDone();
+ assertTrue(callback.mOnCanceledCalled);
+ assertNull(mTestFramework.mCronetEngine);
+ }
+
+ // Returns the contents of byteBuffer, from its position() to its limit(),
+ // as a String. Does not modify byteBuffer's position().
+ private static String bufferContentsToString(ByteBuffer byteBuffer, int start, int end) {
+ // Use a duplicate to avoid modifying byteBuffer.
+ ByteBuffer duplicate = byteBuffer.duplicate();
+ duplicate.position(start);
+ duplicate.limit(end);
+ byte[] contents = new byte[duplicate.remaining()];
+ duplicate.get(contents);
+ return new String(contents);
}
}
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestBase.java b/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestBase.java
index 30c4856..28aafd3 100644
--- a/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestBase.java
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestBase.java
@@ -175,6 +175,22 @@ public class CronetTestBase extends AndroidTestCase {
NativeTestServer.registerHostResolverProc(urlRequestContextAdapter, isLegacyAPI);
}
+ void assertResponseEquals(UrlResponseInfo expected, UrlResponseInfo actual) {
+ assertEquals(expected.getAllHeaders(), actual.getAllHeaders());
+ assertEquals(expected.getAllHeadersAsList(), actual.getAllHeadersAsList());
+ assertEquals(expected.getHttpStatusCode(), actual.getHttpStatusCode());
+ assertEquals(expected.getHttpStatusText(), actual.getHttpStatusText());
+ assertEquals(expected.getUrlChain(), actual.getUrlChain());
+ assertEquals(expected.getUrl(), actual.getUrl());
+ // Transferred bytes and proxy server are not supported in pure java
+ if (!(mCronetTestFramework.mCronetEngine instanceof JavaCronetEngine)) {
+ assertEquals(expected.getReceivedBytesCount(), actual.getReceivedBytesCount());
+ assertEquals(expected.getProxyServer(), actual.getProxyServer());
+ // This is a place where behavior intentionally differs between native and java
+ assertEquals(expected.getNegotiatedProtocol(), actual.getNegotiatedProtocol());
+ }
+ }
+
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CompareDefaultWithCronet {
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java
index 2ac2bc3..23a5854 100644
--- a/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java
@@ -149,22 +149,6 @@ public class CronetUrlRequestTest extends CronetTestBase {
return unknown;
}
- void assertResponseEquals(UrlResponseInfo expected, UrlResponseInfo actual) {
- assertEquals(expected.getAllHeaders(), actual.getAllHeaders());
- assertEquals(expected.getAllHeadersAsList(), actual.getAllHeadersAsList());
- assertEquals(expected.getHttpStatusCode(), actual.getHttpStatusCode());
- assertEquals(expected.getHttpStatusText(), actual.getHttpStatusText());
- assertEquals(expected.getUrlChain(), actual.getUrlChain());
- assertEquals(expected.getUrl(), actual.getUrl());
- // Transferred bytes and proxy server are not supported in pure java
- if (!(mTestFramework.mCronetEngine instanceof JavaCronetEngine)) {
- assertEquals(expected.getReceivedBytesCount(), actual.getReceivedBytesCount());
- assertEquals(expected.getProxyServer(), actual.getProxyServer());
- // This is a place where behavior intentionally differs between native and java
- assertEquals(expected.getNegotiatedProtocol(), actual.getNegotiatedProtocol());
- }
- }
-
/**
* Tests a redirect by running it step-by-step. Also tests that delaying a
* request works as expected. To make sure there are no unexpected pending
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java
index fa9c5d0..5a74621 100644
--- a/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java
@@ -24,7 +24,6 @@ import java.util.HashMap;
*/
public class QuicTest extends CronetTestBase {
private static final String TAG = "cr.QuicTest";
- private static final String[] CERTS_USED = {"quic_test.example.com.crt"};
private CronetTestFramework mTestFramework;
private CronetEngine.Builder mBuilder;
@@ -51,7 +50,7 @@ public class QuicTest extends CronetTestBase {
JSONObject experimentalOptions = new JSONObject().put("QUIC", quicParams);
mBuilder.setExperimentalOptions(experimentalOptions.toString());
- mBuilder.setMockCertVerifierForTesting(MockCertVerifier.createMockCertVerifier(CERTS_USED));
+ mBuilder.setMockCertVerifierForTesting(QuicTestServer.createMockCertVerifier());
mBuilder.setStoragePath(CronetTestFramework.getTestStorage(getContext()));
mBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 1000 * 1024);
}
@@ -141,7 +140,7 @@ public class QuicTest extends CronetTestBase {
JSONObject quicParams = new JSONObject().put("host_whitelist", "test.example.com");
JSONObject experimentalOptions = new JSONObject().put("QUIC", quicParams);
builder.setExperimentalOptions(experimentalOptions.toString());
- builder.setMockCertVerifierForTesting(MockCertVerifier.createMockCertVerifier(CERTS_USED));
+ builder.setMockCertVerifierForTesting(QuicTestServer.createMockCertVerifier());
mTestFramework = startCronetTestFrameworkWithUrlAndCronetEngineBuilder(null, builder);
registerHostResolver(mTestFramework);
TestUrlRequestCallback callback2 = new TestUrlRequestCallback();
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java b/components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java
new file mode 100644
index 0000000..c7a630c
--- /dev/null
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java
@@ -0,0 +1,334 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.net;
+
+import android.os.ConditionVariable;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Callback that tracks information from different callbacks and and has a
+ * method to block thread until the stream completes on another thread.
+ * Allows to cancel, block stream or throw an exception from an arbitrary step.
+ */
+public class TestBidirectionalStreamCallback extends BidirectionalStream.Callback {
+ public UrlResponseInfo mResponseInfo;
+ public CronetException mError;
+
+ public ResponseStep mResponseStep = ResponseStep.NOTHING;
+
+ public boolean mOnErrorCalled = false;
+ public boolean mOnCanceledCalled = false;
+
+ public int mHttpResponseDataLength = 0;
+ public String mResponseAsString = "";
+
+ public UrlResponseInfo.HeaderBlock mTrailers;
+
+ private static final int READ_BUFFER_SIZE = 32 * 1024;
+
+ // When false, the consumer is responsible for all calls into the stream
+ // that advance it.
+ private boolean mAutoAdvance = true;
+
+ // Conditionally fail on certain steps.
+ private FailureType mFailureType = FailureType.NONE;
+ private ResponseStep mFailureStep = ResponseStep.NOTHING;
+
+ // Signals when the stream is done either successfully or not.
+ private final ConditionVariable mDone = new ConditionVariable();
+
+ // Signaled on each step when mAutoAdvance is false.
+ private final ConditionVariable mReadStepBlock = new ConditionVariable();
+ private final ConditionVariable mWriteStepBlock = new ConditionVariable();
+
+ // Executor Service for Cronet callbacks.
+ private final ExecutorService mExecutorService =
+ Executors.newSingleThreadExecutor(new ExecutorThreadFactory());
+ private Thread mExecutorThread;
+
+ // position() of ByteBuffer prior to read() call.
+ private int mBufferPositionBeforeRead;
+
+ // Data to write.
+ private ArrayList<ByteBuffer> mWriteBuffers = new ArrayList<ByteBuffer>();
+
+ private class ExecutorThreadFactory implements ThreadFactory {
+ public Thread newThread(Runnable r) {
+ mExecutorThread = new Thread(r);
+ return mExecutorThread;
+ }
+ }
+
+ public enum ResponseStep {
+ NOTHING,
+ ON_REQUEST_HEADERS_SENT,
+ ON_RESPONSE_STARTED,
+ ON_READ_COMPLETED,
+ ON_WRITE_COMPLETED,
+ ON_TRAILERS,
+ ON_CANCELED,
+ ON_FAILED,
+ ON_SUCCEEDED
+ }
+
+ public enum FailureType {
+ NONE,
+ CANCEL_SYNC,
+ CANCEL_ASYNC,
+ // Same as above, but continues to advance the stream after posting
+ // the cancellation task.
+ CANCEL_ASYNC_WITHOUT_PAUSE,
+ THROW_SYNC
+ }
+
+ public void setAutoAdvance(boolean autoAdvance) {
+ mAutoAdvance = autoAdvance;
+ }
+
+ public void setFailure(FailureType failureType, ResponseStep failureStep) {
+ mFailureStep = failureStep;
+ mFailureType = failureType;
+ }
+
+ public void blockForDone() {
+ mDone.block();
+ }
+
+ public void waitForNextReadStep() {
+ mReadStepBlock.block();
+ mReadStepBlock.close();
+ }
+
+ public void waitForNextWriteStep() {
+ mWriteStepBlock.block();
+ mWriteStepBlock.close();
+ }
+
+ public Executor getExecutor() {
+ return mExecutorService;
+ }
+
+ public void shutdownExecutor() {
+ mExecutorService.shutdown();
+ }
+
+ public void addWriteData(byte[] data) {
+ ByteBuffer writeBuffer = ByteBuffer.allocateDirect(data.length);
+ writeBuffer.put(data);
+ writeBuffer.flip();
+ mWriteBuffers.add(writeBuffer);
+ }
+
+ @Override
+ public void onRequestHeadersSent(BidirectionalStream stream) {
+ assertEquals(mExecutorThread, Thread.currentThread());
+ assertFalse(stream.isDone());
+ assertEquals(ResponseStep.NOTHING, mResponseStep);
+ assertNull(mError);
+
+ mResponseStep = ResponseStep.ON_REQUEST_HEADERS_SENT;
+ if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) {
+ return;
+ }
+ startNextWrite(stream);
+ }
+
+ @Override
+ public void onResponseHeadersReceived(BidirectionalStream stream, UrlResponseInfo info) {
+ assertEquals(mExecutorThread, Thread.currentThread());
+ assertFalse(stream.isDone());
+ assertTrue(mResponseStep == ResponseStep.NOTHING
+ || mResponseStep == ResponseStep.ON_REQUEST_HEADERS_SENT
+ || mResponseStep == ResponseStep.ON_WRITE_COMPLETED);
+ assertNull(mError);
+
+ mResponseStep = ResponseStep.ON_RESPONSE_STARTED;
+ mResponseInfo = info;
+ if (maybeThrowCancelOrPause(stream, mReadStepBlock)) {
+ return;
+ }
+ startNextRead(stream);
+ }
+
+ @Override
+ public void onReadCompleted(
+ BidirectionalStream stream, UrlResponseInfo info, ByteBuffer byteBuffer) {
+ assertEquals(mExecutorThread, Thread.currentThread());
+ assertFalse(stream.isDone());
+ assertTrue(mResponseStep == ResponseStep.ON_RESPONSE_STARTED
+ || mResponseStep == ResponseStep.ON_READ_COMPLETED
+ || mResponseStep == ResponseStep.ON_WRITE_COMPLETED
+ || mResponseStep == ResponseStep.ON_TRAILERS);
+ assertNull(mError);
+
+ mResponseStep = ResponseStep.ON_READ_COMPLETED;
+
+ final int bytesRead = byteBuffer.position() - mBufferPositionBeforeRead;
+ mHttpResponseDataLength += bytesRead;
+ final byte[] lastDataReceivedAsBytes = new byte[bytesRead];
+ // Rewind byteBuffer.position() to pre-read() position.
+ byteBuffer.position(mBufferPositionBeforeRead);
+ // This restores byteBuffer.position() to its value on entrance to
+ // this function.
+ byteBuffer.get(lastDataReceivedAsBytes);
+
+ mResponseAsString += new String(lastDataReceivedAsBytes);
+
+ if (maybeThrowCancelOrPause(stream, mReadStepBlock)) {
+ return;
+ }
+ startNextRead(stream);
+ }
+
+ @Override
+ public void onWriteCompleted(
+ BidirectionalStream stream, UrlResponseInfo info, ByteBuffer buffer) {
+ assertEquals(mExecutorThread, Thread.currentThread());
+ assertFalse(stream.isDone());
+ assertNull(mError);
+ mResponseStep = ResponseStep.ON_WRITE_COMPLETED;
+ if (!mWriteBuffers.isEmpty()) {
+ assertEquals(buffer, mWriteBuffers.get(0));
+ mWriteBuffers.remove(0);
+ }
+ if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) {
+ return;
+ }
+ startNextWrite(stream);
+ }
+
+ @Override
+ public void onResponseTrailersReceived(BidirectionalStream stream, UrlResponseInfo info,
+ UrlResponseInfo.HeaderBlock trailers) {
+ assertEquals(mExecutorThread, Thread.currentThread());
+ assertFalse(stream.isDone());
+ assertNull(mError);
+ mResponseStep = ResponseStep.ON_TRAILERS;
+ mTrailers = trailers;
+ if (maybeThrowCancelOrPause(stream, mReadStepBlock)) {
+ return;
+ }
+ }
+
+ @Override
+ public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) {
+ assertEquals(mExecutorThread, Thread.currentThread());
+ assertTrue(stream.isDone());
+ assertTrue(mResponseStep == ResponseStep.ON_RESPONSE_STARTED
+ || mResponseStep == ResponseStep.ON_READ_COMPLETED
+ || mResponseStep == ResponseStep.ON_WRITE_COMPLETED
+ || mResponseStep == ResponseStep.ON_TRAILERS);
+ assertFalse(mOnErrorCalled);
+ assertFalse(mOnCanceledCalled);
+ assertNull(mError);
+
+ mResponseStep = ResponseStep.ON_SUCCEEDED;
+ mResponseInfo = info;
+ openDone();
+ maybeThrowCancelOrPause(stream, mReadStepBlock);
+ }
+
+ @Override
+ public void onFailed(BidirectionalStream stream, UrlResponseInfo info, CronetException error) {
+ assertEquals(mExecutorThread, Thread.currentThread());
+ assertTrue(stream.isDone());
+ // Shouldn't happen after success.
+ assertTrue(mResponseStep != ResponseStep.ON_SUCCEEDED);
+ // Should happen at most once for a single stream.
+ assertFalse(mOnErrorCalled);
+ assertFalse(mOnCanceledCalled);
+ assertNull(mError);
+ mResponseStep = ResponseStep.ON_FAILED;
+
+ mOnErrorCalled = true;
+ mError = error;
+ openDone();
+ maybeThrowCancelOrPause(stream, mReadStepBlock);
+ }
+
+ @Override
+ public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) {
+ assertEquals(mExecutorThread, Thread.currentThread());
+ assertTrue(stream.isDone());
+ // Should happen at most once for a single stream.
+ assertFalse(mOnCanceledCalled);
+ assertFalse(mOnErrorCalled);
+ assertNull(mError);
+ mResponseStep = ResponseStep.ON_CANCELED;
+
+ mOnCanceledCalled = true;
+ openDone();
+ maybeThrowCancelOrPause(stream, mReadStepBlock);
+ }
+
+ public void startNextRead(BidirectionalStream stream) {
+ startNextRead(stream, ByteBuffer.allocateDirect(READ_BUFFER_SIZE));
+ }
+
+ public void startNextRead(BidirectionalStream stream, ByteBuffer buffer) {
+ mBufferPositionBeforeRead = buffer.position();
+ stream.read(buffer);
+ }
+
+ public void startNextWrite(BidirectionalStream stream) {
+ if (!mWriteBuffers.isEmpty()) {
+ boolean isLastBuffer = mWriteBuffers.size() == 1;
+ stream.write(mWriteBuffers.get(0), isLastBuffer);
+ }
+ }
+
+ public boolean isDone() {
+ // It's not mentioned by the Android docs, but block(0) seems to block
+ // indefinitely, so have to block for one millisecond to get state
+ // without blocking.
+ return mDone.block(1);
+ }
+
+ protected void openDone() {
+ mDone.open();
+ }
+
+ /**
+ * Returns {@code false} if the callback should continue to advance the
+ * stream.
+ */
+ private boolean maybeThrowCancelOrPause(
+ final BidirectionalStream stream, ConditionVariable stepBlock) {
+ if (mResponseStep != mFailureStep || mFailureType == FailureType.NONE) {
+ if (!mAutoAdvance) {
+ stepBlock.open();
+ return true;
+ }
+ return false;
+ }
+
+ if (mFailureType == FailureType.THROW_SYNC) {
+ throw new IllegalStateException("Callback Exception.");
+ }
+ Runnable task = new Runnable() {
+ public void run() {
+ stream.cancel();
+ }
+ };
+ if (mFailureType == FailureType.CANCEL_ASYNC
+ || mFailureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE) {
+ getExecutor().execute(task);
+ } else {
+ task.run();
+ }
+ return mFailureType != FailureType.CANCEL_ASYNC_WITHOUT_PAUSE;
+ }
+}
diff --git a/components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java b/components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java
new file mode 100644
index 0000000..2b70a78
--- /dev/null
+++ b/components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java
@@ -0,0 +1,287 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.net;
+
+import org.chromium.base.Log;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import static io.netty.buffer.Unpooled.copiedBuffer;
+import static io.netty.buffer.Unpooled.unreleasableBuffer;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.netty.handler.logging.LogLevel.INFO;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder;
+import io.netty.handler.codec.http2.DefaultHttp2Headers;
+import io.netty.handler.codec.http2.Http2ConnectionDecoder;
+import io.netty.handler.codec.http2.Http2ConnectionEncoder;
+import io.netty.handler.codec.http2.Http2ConnectionHandler;
+import io.netty.handler.codec.http2.Http2Exception;
+import io.netty.handler.codec.http2.Http2Flags;
+import io.netty.handler.codec.http2.Http2FrameListener;
+import io.netty.handler.codec.http2.Http2FrameLogger;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.handler.codec.http2.Http2Settings;
+import io.netty.util.CharsetUtil;
+
+/**
+ * HTTP/2 test handler for Cronet BidirectionalStream tests.
+ */
+public final class Http2TestHandler extends Http2ConnectionHandler implements Http2FrameListener {
+ // Some Url Paths that have special meaning.
+ public static final String ECHO_ALL_HEADERS_PATH = "/echoallheaders";
+ public static final String ECHO_HEADER_PATH = "/echoheader";
+ public static final String ECHO_METHOD_PATH = "/echomethod";
+ public static final String ECHO_STREAM_PATH = "/echostream";
+ public static final String ECHO_TRAILERS_PATH = "/echotrailers";
+
+ private static final String TAG = "cr_Http2TestHandler";
+ private static final Http2FrameLogger sLogger =
+ new Http2FrameLogger(INFO, Http2TestHandler.class);
+ private static final ByteBuf RESPONSE_BYTES =
+ unreleasableBuffer(copiedBuffer("HTTP/2 Test Server", CharsetUtil.UTF_8));
+
+ private HashMap<Integer, RequestResponder> mResponderMap = new HashMap<>();
+
+ /**
+ * Builder for HTTP/2 test handler.
+ */
+ public static final class Builder
+ extends AbstractHttp2ConnectionHandlerBuilder<Http2TestHandler, Builder> {
+ public Builder() {
+ frameLogger(sLogger);
+ }
+
+ @Override
+ public Http2TestHandler build() {
+ return super.build();
+ }
+
+ @Override
+ protected Http2TestHandler build(Http2ConnectionDecoder decoder,
+ Http2ConnectionEncoder encoder, Http2Settings initialSettings) {
+ Http2TestHandler handler = new Http2TestHandler(decoder, encoder, initialSettings);
+ frameListener(handler);
+ return handler;
+ }
+ }
+
+ private class RequestResponder {
+ void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
+ Http2Headers headers) {
+ encoder().writeHeaders(ctx, streamId, createResponseHeadersFromRequestHeaders(headers),
+ 0, false, ctx.newPromise());
+ ctx.flush();
+ }
+
+ int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
+ boolean endOfStream) {
+ int processed = data.readableBytes() + padding;
+ encoder().writeData(ctx, streamId, data.retain(), 0, true, ctx.newPromise());
+ ctx.flush();
+ return processed;
+ }
+
+ void sendResponseString(ChannelHandlerContext ctx, int streamId, String responseString) {
+ ByteBuf content = ctx.alloc().buffer();
+ ByteBufUtil.writeAscii(content, responseString);
+ encoder().writeHeaders(
+ ctx, streamId, createDefaultResponseHeaders(), 0, false, ctx.newPromise());
+ encoder().writeData(ctx, streamId, content, 0, true, ctx.newPromise());
+ ctx.flush();
+ }
+ }
+
+ private class EchoStreamResponder extends RequestResponder {
+ @Override
+ void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
+ Http2Headers headers) {
+ // Send a frame for the response headers.
+ encoder().writeHeaders(ctx, streamId, createResponseHeadersFromRequestHeaders(headers),
+ 0, endOfStream, ctx.newPromise());
+ ctx.flush();
+ }
+
+ @Override
+ int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
+ boolean endOfStream) {
+ int processed = data.readableBytes() + padding;
+ encoder().writeData(ctx, streamId, data.retain(), 0, endOfStream, ctx.newPromise());
+ ctx.flush();
+ return processed;
+ }
+ }
+
+ private class EchoHeaderResponder extends RequestResponder {
+ @Override
+ void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
+ Http2Headers headers) {
+ String[] splitPath = headers.path().toString().split("\\?");
+ if (splitPath.length <= 1) {
+ sendResponseString(ctx, streamId, "Header name not found.");
+ return;
+ }
+
+ String headerName = splitPath[1].toLowerCase(Locale.US);
+ if (headers.get(headerName) == null) {
+ sendResponseString(ctx, streamId, "Header not found:" + headerName);
+ return;
+ }
+
+ sendResponseString(ctx, streamId, headers.get(headerName).toString());
+ }
+ }
+
+ private class EchoAllHeadersResponder extends RequestResponder {
+ @Override
+ void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
+ Http2Headers headers) {
+ StringBuilder response = new StringBuilder();
+ for (Map.Entry<CharSequence, CharSequence> header : headers) {
+ response.append(header.getKey() + ": " + header.getValue() + "\r\n");
+ }
+ sendResponseString(ctx, streamId, response.toString());
+ }
+ }
+
+ private class EchoMethodResponder extends RequestResponder {
+ @Override
+ void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
+ Http2Headers headers) {
+ sendResponseString(ctx, streamId, headers.method().toString());
+ }
+ }
+
+ private class EchoTrailersResponder extends RequestResponder {
+ @Override
+ void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
+ Http2Headers headers) {
+ encoder().writeHeaders(
+ ctx, streamId, createDefaultResponseHeaders(), 0, false, ctx.newPromise());
+ encoder().writeData(
+ ctx, streamId, RESPONSE_BYTES.duplicate(), 0, false, ctx.newPromise());
+ Http2Headers responseTrailers = createResponseHeadersFromRequestHeaders(headers).add(
+ "trailer", "value1", "Value2");
+ encoder().writeHeaders(ctx, streamId, responseTrailers, 0, true, ctx.newPromise());
+ ctx.flush();
+ }
+ }
+
+ private static Http2Headers createDefaultResponseHeaders() {
+ return new DefaultHttp2Headers().status(OK.codeAsText());
+ }
+
+ private static Http2Headers createResponseHeadersFromRequestHeaders(
+ Http2Headers requestHeaders) {
+ // Create response headers by echoing request headers.
+ Http2Headers responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText());
+ for (Map.Entry<CharSequence, CharSequence> header : requestHeaders) {
+ if (!header.getKey().toString().startsWith(":")) {
+ responseHeaders.add("echo-" + header.getKey(), header.getValue());
+ }
+ }
+
+ responseHeaders.add("echo-method", requestHeaders.get(":method").toString());
+ return responseHeaders;
+ }
+
+ private Http2TestHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
+ Http2Settings initialSettings) {
+ super(decoder, encoder, initialSettings);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ super.exceptionCaught(ctx, cause);
+ Log.e(TAG, "An exception was caught", cause);
+ ctx.close();
+ throw new Exception("Exception Caught", cause);
+ }
+
+ @Override
+ public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
+ boolean endOfStream) throws Http2Exception {
+ RequestResponder responder = mResponderMap.get(streamId);
+ if (endOfStream) {
+ mResponderMap.remove(streamId);
+ }
+ return responder.onDataRead(ctx, streamId, data, padding, endOfStream);
+ }
+
+ @Override
+ public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
+ int padding, boolean endOfStream) throws Http2Exception {
+ String path = headers.path().toString();
+ RequestResponder responder;
+ if (path.startsWith(ECHO_STREAM_PATH)) {
+ responder = new EchoStreamResponder();
+ } else if (path.startsWith(ECHO_TRAILERS_PATH)) {
+ responder = new EchoTrailersResponder();
+ } else if (path.startsWith(ECHO_ALL_HEADERS_PATH)) {
+ responder = new EchoAllHeadersResponder();
+ } else if (path.startsWith(ECHO_HEADER_PATH)) {
+ responder = new EchoHeaderResponder();
+ } else if (path.startsWith(ECHO_METHOD_PATH)) {
+ responder = new EchoMethodResponder();
+ } else {
+ responder = new RequestResponder();
+ }
+
+ responder.onHeadersRead(ctx, streamId, endOfStream, headers);
+
+ if (!endOfStream) {
+ mResponderMap.put(streamId, responder);
+ }
+ }
+
+ @Override
+ public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
+ int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream)
+ throws Http2Exception {
+ onHeadersRead(ctx, streamId, headers, padding, endOfStream);
+ }
+
+ @Override
+ public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency,
+ short weight, boolean exclusive) throws Http2Exception {}
+
+ @Override
+ public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode)
+ throws Http2Exception {}
+
+ @Override
+ public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {}
+
+ @Override
+ public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings)
+ throws Http2Exception {}
+
+ @Override
+ public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {}
+
+ @Override
+ public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {}
+
+ @Override
+ public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
+ Http2Headers headers, int padding) throws Http2Exception {}
+
+ @Override
+ public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode,
+ ByteBuf debugData) throws Http2Exception {}
+
+ @Override
+ public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
+ throws Http2Exception {}
+
+ @Override
+ public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId,
+ Http2Flags flags, ByteBuf payload) throws Http2Exception {}
+}
diff --git a/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java b/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java
new file mode 100644
index 0000000..1af837c
--- /dev/null
+++ b/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java
@@ -0,0 +1,179 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.net;
+
+import android.content.Context;
+import android.os.ConditionVariable;
+
+import org.chromium.base.Log;
+import org.chromium.net.test.util.CertTestUtil;
+
+import java.io.File;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http2.Http2SecurityUtil;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.ApplicationProtocolConfig;
+import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
+import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
+import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
+import io.netty.handler.ssl.ApplicationProtocolNames;
+import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
+import io.netty.handler.ssl.OpenSslServerContext;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SupportedCipherSuiteFilter;
+
+/**
+ * Wrapper class to start a HTTP/2 test server.
+ */
+public final class Http2TestServer {
+ private static final ConditionVariable sBlock = new ConditionVariable();
+ private static Channel sServerChannel;
+ private static final String TAG = "Http2TestServer";
+
+ private static final String HOST = "127.0.0.1";
+ // Server port.
+ private static final int PORT = 8443;
+
+ public static boolean shutdownHttp2TestServer() throws Exception {
+ if (sServerChannel != null) {
+ sServerChannel.close();
+ sServerChannel = null;
+ return true;
+ }
+ return false;
+ }
+
+ public static String getServerHost() {
+ return HOST;
+ }
+
+ public static int getServerPort() {
+ return PORT;
+ }
+
+ public static String getServerUrl() {
+ return "https://" + HOST + ":" + PORT;
+ }
+
+ public static String getEchoAllHeadersUrl() {
+ return getServerUrl() + Http2TestHandler.ECHO_ALL_HEADERS_PATH;
+ }
+
+ public static String getEchoHeaderUrl(String headerName) {
+ return getServerUrl() + Http2TestHandler.ECHO_HEADER_PATH + "?" + headerName;
+ }
+
+ public static String getEchoMethodUrl() {
+ return getServerUrl() + Http2TestHandler.ECHO_METHOD_PATH;
+ }
+
+ /**
+ * @return url of the server resource which will echo every received stream data frame.
+ */
+ public static String getEchoStreamUrl() {
+ return getServerUrl() + Http2TestHandler.ECHO_STREAM_PATH;
+ }
+
+ /**
+ * @return url of the server resource which will echo request headers as response trailers.
+ */
+ public static String getEchoTrailersUrl() {
+ return getServerUrl() + Http2TestHandler.ECHO_TRAILERS_PATH;
+ }
+
+ public static boolean startHttp2TestServer(
+ Context context, String certFileName, String keyFileName) throws Exception {
+ new Thread(
+ new Http2TestServerRunnable(new File(CertTestUtil.CERTS_DIRECTORY + certFileName),
+ new File(CertTestUtil.CERTS_DIRECTORY + keyFileName)))
+ .start();
+ sBlock.block();
+ return true;
+ }
+
+ private Http2TestServer() {}
+
+ private static class Http2TestServerRunnable implements Runnable {
+ private final SslContext mSslCtx;
+
+ Http2TestServerRunnable(File certFile, File keyFile) throws Exception {
+ ApplicationProtocolConfig applicationProtocolConfig = new ApplicationProtocolConfig(
+ Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE,
+ SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2);
+
+ mSslCtx = new OpenSslServerContext(certFile, keyFile, null, null,
+ Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE,
+ applicationProtocolConfig, 0, 0);
+ }
+
+ public void run() {
+ try {
+ // Configure the server.
+ EventLoopGroup group = new NioEventLoopGroup();
+ try {
+ ServerBootstrap b = new ServerBootstrap();
+ b.option(ChannelOption.SO_BACKLOG, 1024);
+ b.group(group)
+ .channel(NioServerSocketChannel.class)
+ .handler(new LoggingHandler(LogLevel.INFO))
+ .childHandler(new Http2ServerInitializer(mSslCtx));
+
+ sServerChannel = b.bind(PORT).sync().channel();
+ Log.i(TAG, "Netty HTTP/2 server started on " + getServerUrl());
+ sBlock.open();
+ sServerChannel.closeFuture().sync();
+ } finally {
+ group.shutdownGracefully();
+ }
+ Log.i(TAG, "Stopped Http2TestServerRunnable!");
+ } catch (Exception e) {
+ Log.e(TAG, e.toString());
+ }
+ }
+ }
+
+ /**
+ * Sets up the Netty pipeline for the test server.
+ */
+ private static class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
+ private final SslContext mSslCtx;
+
+ public Http2ServerInitializer(SslContext sslCtx) {
+ this.mSslCtx = sslCtx;
+ }
+
+ @Override
+ public void initChannel(SocketChannel ch) {
+ ch.pipeline().addLast(mSslCtx.newHandler(ch.alloc()), new Http2NegotiationHandler());
+ }
+ }
+
+ private static class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler {
+ protected Http2NegotiationHandler() {
+ super(ApplicationProtocolNames.HTTP_1_1);
+ }
+
+ @Override
+ protected void configurePipeline(ChannelHandlerContext ctx, String protocol)
+ throws Exception {
+ if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
+ ctx.pipeline().addLast(new Http2TestHandler.Builder().build());
+ return;
+ }
+
+ throw new IllegalStateException("unknown protocol: " + protocol);
+ }
+ }
+}
diff --git a/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java b/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java
index 44c414f..1344cc4 100644
--- a/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java
+++ b/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java
@@ -19,6 +19,10 @@ public final class QuicTestServer {
private static final ConditionVariable sBlock = new ConditionVariable();
private static final String TAG = "cr.QuicTestServer";
+ private static final String CERT_USED = "quic_test.example.com.crt";
+ private static final String KEY_USED = "quic_test.example.com.key";
+ private static final String[] CERTS_USED = {CERT_USED};
+
public static void startQuicTestServer(Context context) {
TestFilesInstaller.installIfNeeded(context);
nativeStartQuicTestServer(TestFilesInstaller.getInstalledPath(context));
@@ -42,6 +46,18 @@ public final class QuicTestServer {
return nativeGetServerPort();
}
+ public static final String getServerCert() {
+ return CERT_USED;
+ }
+
+ public static final String getServerCertKey() {
+ return KEY_USED;
+ }
+
+ public static long createMockCertVerifier() {
+ return MockCertVerifier.createMockCertVerifier(CERTS_USED);
+ }
+
@CalledByNative
private static void onServerStarted() {
Log.i(TAG, "Quic server started.");
diff --git a/components/cronet/cronet_static.gypi b/components/cronet/cronet_static.gypi
index 22d2a6f..1557f8a 100644
--- a/components/cronet/cronet_static.gypi
+++ b/components/cronet/cronet_static.gypi
@@ -7,6 +7,7 @@
'dependencies': [
'../base/base.gyp:base',
'../base/third_party/dynamic_annotations/dynamic_annotations.gyp:dynamic_annotations',
+ 'cronet_features',
'cronet_jni_headers',
'cronet_url_request_java',
'cronet_version',
@@ -72,5 +73,15 @@
],
}
],
+ # If Bidirectional Stream support is enabled, add the following sources.
+ # Dependencies are target-specific and are not included here.
+ ['enable_bidirectional_stream==1',
+ {
+ 'sources': [
+ 'android/cronet_bidirectional_stream_adapter.cc',
+ 'android/cronet_bidirectional_stream_adapter.h',
+ ],
+ }
+ ],
],
}
diff --git a/components/cronet/tools/cr_cronet.py b/components/cronet/tools/cr_cronet.py
index 7d87b6c..6a14515 100755
--- a/components/cronet/tools/cr_cronet.py
+++ b/components/cronet/tools/cr_cronet.py
@@ -51,6 +51,7 @@ def main():
parser = argparse.ArgumentParser()
parser.add_argument('command',
choices=['gyp',
+ 'gn',
'sync',
'build',
'install',
@@ -67,16 +68,23 @@ def main():
print options
print extra_options_list
gyp_defines = 'GYP_DEFINES="OS=android enable_websockets=0 '+ \
- 'disable_file_support=1 disable_ftp_support=1" '
+ 'disable_file_support=1 disable_ftp_support=1 '+ \
+ 'enable_bidirectional_stream=1"'
+ gn_args = 'target_os="android" enable_websockets=false '+ \
+ 'disable_file_support=true disable_ftp_support=true '+ \
+ 'enable_bidirectional_stream=false'
out_dir = 'out/Debug'
release_arg = ''
extra_options = ' '.join(extra_options_list)
if options.release:
out_dir = 'out/Release'
release_arg = ' --release'
+ gn_args += ' is_debug=false '
if (options.command=='gyp'):
return run (gyp_defines + ' gclient runhooks')
+ if (options.command=='gn'):
+ return run ('gn gen ' + out_dir + ' --args=\'' + gn_args + '\'')
if (options.command=='sync'):
return run ('git pull --rebase && ' + gyp_defines + ' gclient sync')
if (options.command=='build'):