diff --git a/apps/Development/AndroidManifest.xml b/apps/Development/AndroidManifest.xml
index c5133a538..016a245ea 100644
--- a/apps/Development/AndroidManifest.xml
+++ b/apps/Development/AndroidManifest.xml
@@ -112,6 +112,12 @@
+
+
+
+
+
+
diff --git a/apps/Development/res/layout/http_engine_activity.xml b/apps/Development/res/layout/http_engine_activity.xml
new file mode 100644
index 000000000..079597799
--- /dev/null
+++ b/apps/Development/res/layout/http_engine_activity.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/Development/res/layout/http_engine_dialog.xml b/apps/Development/res/layout/http_engine_dialog.xml
new file mode 100644
index 000000000..7c2a9ee8b
--- /dev/null
+++ b/apps/Development/res/layout/http_engine_dialog.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/Development/res/values/dimens.xml b/apps/Development/res/values/dimens.xml
new file mode 100644
index 000000000..333a542ea
--- /dev/null
+++ b/apps/Development/res/values/dimens.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ 16dp
+ 16dp
+
diff --git a/apps/Development/res/values/strings.xml b/apps/Development/res/values/strings.xml
index 1ee1c18be..d7ece21d7 100644
--- a/apps/Development/res/values/strings.xml
+++ b/apps/Development/res/values/strings.xml
@@ -225,4 +225,8 @@
Scan SD card
# of albums
Insert %1s albums
+
+
+ Enter a URL
+ Enter post data (leave it blank for GET request)
diff --git a/apps/Development/src/com/android/development/HttpEngineActivity.java b/apps/Development/src/com/android/development/HttpEngineActivity.java
new file mode 100644
index 000000000..b239136ed
--- /dev/null
+++ b/apps/Development/src/com/android/development/HttpEngineActivity.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.development;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.net.http.HttpEngine;
+import android.net.http.HttpException;
+import android.net.http.UploadDataProvider;
+import android.net.http.UploadDataSink;
+import android.net.http.UrlRequest;
+import android.net.http.UrlResponseInfo;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * Activity for managing HttpEngine interactions.
+ */
+public class HttpEngineActivity extends Activity {
+ private static final String TAG = HttpEngineActivity.class.getSimpleName();
+
+ private HttpEngine mHttpEngine;
+
+ private String mUrl;
+ private TextView mResultText;
+ private TextView mReceiveDataText;
+
+ class SimpleUrlRequestCallback implements UrlRequest.Callback {
+ private ByteArrayOutputStream mBytesReceived = new ByteArrayOutputStream();
+ private WritableByteChannel mReceiveChannel = Channels.newChannel(mBytesReceived);
+
+ @Override
+ public void onRedirectReceived(
+ UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
+ Log.i(TAG, "****** onRedirectReceived ******");
+ request.followRedirect();
+ }
+
+ @Override
+ public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
+ Log.i(TAG, "****** Response Started ******");
+ Log.i(TAG, "*** Headers Are *** " + info.getHeaders());
+
+ request.read(ByteBuffer.allocateDirect(32 * 1024));
+ }
+
+ @Override
+ public void onReadCompleted(
+ UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {
+ byteBuffer.flip();
+ Log.i(TAG, "****** onReadCompleted ******" + byteBuffer);
+
+ try {
+ mReceiveChannel.write(byteBuffer);
+ } catch (IOException e) {
+ Log.i(TAG, "IOException during ByteBuffer read. Details: ", e);
+ }
+ byteBuffer.clear();
+ request.read(byteBuffer);
+ }
+
+ @Override
+ public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
+ Log.i(TAG, "****** Request Completed, status code is " + info.getHttpStatusCode()
+ + ", total received bytes is " + info.getReceivedByteCount());
+
+ final String receivedData = mBytesReceived.toString();
+ final String url = info.getUrl();
+ final String text = "Completed " + url + " (" + info.getHttpStatusCode() + ")";
+ HttpEngineActivity.this.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mResultText.setText(text);
+ mReceiveDataText.setText(receivedData);
+ promptForURL(url);
+ }
+ });
+ }
+
+ @Override
+ public void onFailed(UrlRequest request, UrlResponseInfo info, HttpException error) {
+ Log.i(TAG, "****** onFailed, error is: " + error.getMessage());
+
+ final String url = mUrl;
+ final String text = "Failed " + mUrl + " (" + error.getMessage() + ")";
+ HttpEngineActivity.this.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mResultText.setText(text);
+ promptForURL(url);
+ }
+ });
+ }
+
+ @Override
+ public void onCanceled(UrlRequest request, UrlResponseInfo info) {
+ Log.i(TAG, "****** onCanceled ******");
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.http_engine_activity);
+ mResultText = (TextView) findViewById(R.id.resultView);
+ mReceiveDataText = (TextView) findViewById(R.id.dataView);
+ mReceiveDataText.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ promptForURL(mUrl);
+ }
+ });
+
+ HttpEngine.Builder myBuilder = new HttpEngine.Builder(this);
+ myBuilder.setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_IN_MEMORY, 100 * 1024)
+ .setEnableHttp2(true)
+ .setEnableQuic(true);
+
+ mHttpEngine = myBuilder.build();
+
+ String appUrl = (getIntent() != null ? getIntent().getDataString() : null);
+ if (appUrl == null) {
+ promptForURL("https://");
+ } else {
+ startWithURL(appUrl);
+ }
+ }
+
+ private void promptForURL(String url) {
+ Log.i(TAG, "No URL provided via intent, prompting user...");
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+ alert.setTitle("Enter a URL");
+ LayoutInflater inflater = getLayoutInflater();
+ View alertView = inflater.inflate(R.layout.http_engine_dialog, null);
+ final EditText urlInput = (EditText) alertView.findViewById(R.id.urlText);
+ urlInput.setText(url);
+ final EditText postInput = (EditText) alertView.findViewById(R.id.postText);
+ alert.setView(alertView);
+
+ alert.setPositiveButton("Load", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int button) {
+ String url = urlInput.getText().toString();
+ String postData = postInput.getText().toString();
+ startWithURL(url, postData);
+ }
+ });
+ alert.show();
+ }
+
+ private void applyPostDataToUrlRequestBuilder(
+ UrlRequest.Builder builder, Executor executor, String postData) {
+ if (postData != null && postData.length() > 0) {
+ builder.setHttpMethod("POST");
+ builder.addHeader("Content-Type", "application/x-www-form-urlencoded");
+ // TODO: make android.net.http.apihelpers.UploadDataProviders accessible.
+ builder.setUploadDataProvider(new UploadDataProvider() {
+ @Override
+ public long getLength() {
+ return postData.length();
+ }
+
+ @Override
+ public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) {
+ byteBuffer.put(postData.getBytes());
+ uploadDataSink.onReadSucceeded(/*finalChunk*/ false);
+ }
+
+ @Override
+ public void rewind(UploadDataSink uploadDataSink) {
+ // noop
+ uploadDataSink.onRewindSucceeded();
+ }
+ }, executor);
+ }
+ }
+
+ private void startWithURL(String url) {
+ startWithURL(url, null);
+ }
+
+ private void startWithURL(String url, String postData) {
+ Log.i(TAG, "UrlRequest started: " + url);
+ mUrl = url;
+
+ Executor executor = Executors.newSingleThreadExecutor();
+ UrlRequest.Callback callback = new SimpleUrlRequestCallback();
+ UrlRequest.Builder builder = mHttpEngine.newUrlRequestBuilder(url, executor, callback);
+ applyPostDataToUrlRequestBuilder(builder, executor, postData);
+ builder.build().start();
+ }
+}