From 33118abaa9ec379773e55c1a05528ad26ee9e69d Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Tue, 27 Jun 2023 15:59:11 -0700 Subject: [PATCH] Merge cronet sample app into Development app So we have a way to play with the android.net.http APIs. This is essentially copied from https://source.chromium.org/chromium/chromium/src/+/main:components/cronet/android/sample/ with minor modifications to support android.net.http APIs (mainly, renaming APIs and working around the fact that apihelpers are not available to apps). Test: adb install -r $OUT/system/app/Development/Development.apk Change-Id: I8fdb2f64f110551edd39fab074abaf84e76b010f --- apps/Development/AndroidManifest.xml | 6 + .../res/layout/http_engine_activity.xml | 39 ++++ .../res/layout/http_engine_dialog.xml | 56 +++++ apps/Development/res/values/dimens.xml | 20 ++ apps/Development/res/values/strings.xml | 4 + .../development/HttpEngineActivity.java | 219 ++++++++++++++++++ 6 files changed, 344 insertions(+) create mode 100644 apps/Development/res/layout/http_engine_activity.xml create mode 100644 apps/Development/res/layout/http_engine_dialog.xml create mode 100644 apps/Development/res/values/dimens.xml create mode 100644 apps/Development/src/com/android/development/HttpEngineActivity.java 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(); + } +}