2 * Copyright © 2017-2018 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
6 * Privacy Browser is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.activities;
22 import android.annotation.SuppressLint;
23 import android.app.Activity;
24 import android.app.DialogFragment;
25 import android.content.Context;
26 import android.content.SharedPreferences;
27 import android.graphics.Typeface;
28 import android.os.AsyncTask;
29 import android.os.Build;
30 import android.os.Bundle;
31 import android.os.LocaleList;
32 import android.preference.PreferenceManager;
33 import android.support.v4.app.NavUtils;
34 import android.support.v7.app.ActionBar;
35 import android.support.v7.app.AppCompatActivity;
36 import android.support.v7.widget.Toolbar;
37 import android.text.SpannableStringBuilder;
38 import android.text.Spanned;
39 import android.text.style.ForegroundColorSpan;
40 import android.text.style.StyleSpan;
41 import android.view.KeyEvent;
42 import android.view.Menu;
43 import android.view.MenuItem;
44 import android.view.View;
45 import android.view.WindowManager;
46 import android.view.inputmethod.InputMethodManager;
47 import android.webkit.CookieManager;
48 import android.widget.EditText;
49 import android.widget.ProgressBar;
50 import android.widget.TextView;
52 import com.stoutner.privacybrowser.R;
53 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog;
55 import java.io.BufferedInputStream;
56 import java.io.ByteArrayOutputStream;
57 import java.io.IOException;
58 import java.io.InputStream;
59 import java.net.HttpURLConnection;
61 import java.util.Locale;
63 public class ViewSourceActivity extends AppCompatActivity {
64 // `activity` is used in `onCreate()` and `goBack()`.
65 private Activity activity;
67 // The color spans are used in `onCreate()` and `highlightUrlText()`.
68 private ForegroundColorSpan redColorSpan;
69 private ForegroundColorSpan initialGrayColorSpan;
70 private ForegroundColorSpan finalGrayColorSpan;
73 protected void onCreate(Bundle savedInstanceState) {
74 // Disable screenshots if not allowed.
75 if (!MainWebViewActivity.allowScreenshots) {
76 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
80 if (MainWebViewActivity.darkTheme) {
81 setTheme(R.style.PrivacyBrowserDark);
83 setTheme(R.style.PrivacyBrowserLight);
86 // Run the default commands.
87 super.onCreate(savedInstanceState);
89 // Store a handle for the current activity.
92 // Set the content view.
93 setContentView(R.layout.view_source_coordinatorlayout);
95 // `SupportActionBar` from `android.support.v7.app.ActionBar` must be used until the minimum API is >= 21.
96 Toolbar viewSourceAppBar = findViewById(R.id.view_source_toolbar);
97 setSupportActionBar(viewSourceAppBar);
100 final ActionBar appBar = getSupportActionBar();
102 // Remove the incorrect warning in Android Studio that appBar might be null.
103 assert appBar != null;
105 // Add the custom layout to the app bar.
106 appBar.setCustomView(R.layout.view_source_app_bar);
107 appBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
109 // Get a handle for the url text box.
110 EditText urlEditText = findViewById(R.id.url_edittext);
112 // Get the formatted URL string from the main activity.
113 String formattedUrlString = MainWebViewActivity.formattedUrlString;
115 // Populate the URL text box.
116 urlEditText.setText(formattedUrlString);
118 // Initialize the foreground color spans for highlighting the URLs. We have to use the deprecated `getColor()` until API >= 23.
119 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
120 initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
121 finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
123 // Apply text highlighting to the URL.
126 // Get a handle for the input method manager, which is used to hide the keyboard.
127 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
129 // Let Android Studio know that we aren't worried about the input method manager being null.
130 assert inputMethodManager != null;
132 // Remove the formatting from the URL when the user is editing the text.
133 urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
134 if (hasFocus) { // The user is editing `urlTextBox`.
135 // Remove the highlighting.
136 urlEditText.getText().removeSpan(redColorSpan);
137 urlEditText.getText().removeSpan(initialGrayColorSpan);
138 urlEditText.getText().removeSpan(finalGrayColorSpan);
139 } else { // The user has stopped editing `urlTextBox`.
140 // Hide the soft keyboard.
141 inputMethodManager.hideSoftInputFromWindow(urlEditText.getWindowToken(), 0);
143 // Reapply the highlighting.
150 // Set the go button on the keyboard to request new source data.
151 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
152 // Request new source data if the enter key was pressed.
153 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
154 // Hide the soft keyboard.
155 inputMethodManager.hideSoftInputFromWindow(urlEditText.getWindowToken(), 0);
157 // Remove the focus from the URL box.
158 urlEditText.clearFocus();
160 // Get new source data for the current URL.
161 new GetSource().execute(urlEditText.getText().toString());
163 // Consume the key press.
166 // Do not consume the key press.
171 // Get the source as an `AsyncTask`.
172 new GetSource().execute(formattedUrlString);
176 public boolean onCreateOptionsMenu(Menu menu) {
177 // Inflate the menu; this adds items to the action bar if it is present.
178 getMenuInflater().inflate(R.menu.view_source_options_menu, menu);
185 public boolean onOptionsItemSelected(MenuItem menuItem) {
186 // Get a handle for the about alert dialog.
187 DialogFragment aboutDialogFragment = new AboutViewSourceDialog();
189 // Show the about alert dialog.
190 aboutDialogFragment.show(getFragmentManager(), getString(R.string.about));
192 // Consume the event.
196 public void goBack(View view) {
198 NavUtils.navigateUpFromSameTask(activity);
201 private void highlightUrlText() {
202 // Get a handle for the URL EditText.
203 EditText urlEditText = findViewById(R.id.url_edittext);
206 String urlString = urlEditText.getText().toString();
208 // Highlight the beginning of the URL.
209 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
210 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
211 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
212 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
215 // Get the index of the `/` immediately after the domain name.
216 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
218 // De-emphasize the text after the domain name.
219 if (endOfDomainName > 0) {
220 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
224 // The first `String` declares the parameters. The `Void` does not declare progress units. The last `String` contains the results.
225 // `StaticFieldLeaks` are suppressed so that Android Studio doesn't complain about running an AsyncTask in a non-static context.
226 @SuppressLint("StaticFieldLeak")
227 private class GetSource extends AsyncTask<String, Void, String> {
228 // The class variables pass information from `doInBackground()` to `onPostExecute()`.
229 SpannableStringBuilder responseMessageBuilder;
230 SpannableStringBuilder requestHeadersBuilder;
231 SpannableStringBuilder responseHeadersBuilder;
233 // `onPreExecute()` operates on the UI thread.
235 protected void onPreExecute() {
236 // Get a handle for the progress bar.
237 ProgressBar progressBar = findViewById(R.id.progress_bar);
239 // Make the progress bar visible.
240 progressBar.setVisibility(View.VISIBLE);
242 // Set the progress bar to be indeterminate.
243 progressBar.setIndeterminate(true);
247 protected String doInBackground(String... formattedUrlString) {
248 // Initialize the response body `String`.
249 String responseBodyString = "";
251 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
253 // Get the current URL from the main activity.
254 URL url = new URL(formattedUrlString[0]);
256 // Open a connection to the URL. No data is actually sent at this point.
257 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
259 // Instantiate the variables necessary to build the request headers.
260 requestHeadersBuilder = new SpannableStringBuilder();
261 int oldRequestHeadersBuilderLength;
262 int newRequestHeadersBuilderLength;
265 // Set the `Host` header property.
266 httpUrlConnection.setRequestProperty("Host", url.getHost());
268 // Add the `Host` header to the string builder and format the text.
269 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
270 requestHeadersBuilder.append("Host", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
271 } else { // Older versions not so much.
272 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
273 requestHeadersBuilder.append("Host");
274 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
275 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
277 requestHeadersBuilder.append(": ");
278 requestHeadersBuilder.append(url.getHost());
281 // Set the `Connection` header property.
282 httpUrlConnection.setRequestProperty("Connection", "keep-alive");
284 // Add the `Connection` header to the string builder and format the text.
285 requestHeadersBuilder.append(System.getProperty("line.separator"));
286 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
287 requestHeadersBuilder.append("Connection", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
288 } else { // Older versions not so much.
289 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
290 requestHeadersBuilder.append("Connection");
291 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
292 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
294 requestHeadersBuilder.append(": keep-alive");
297 // Get the current `User-Agent` string.
298 String userAgentString = MainWebViewActivity.appliedUserAgentString;
300 // Set the `User-Agent` header property.
301 httpUrlConnection.setRequestProperty("User-Agent", userAgentString);
303 // Add the `User-Agent` header to the string builder and format the text.
304 requestHeadersBuilder.append(System.getProperty("line.separator"));
305 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
306 requestHeadersBuilder.append("User-Agent", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
307 } else { // Older versions not so much.
308 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
309 requestHeadersBuilder.append("User-Agent");
310 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
311 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
313 requestHeadersBuilder.append(": ");
314 requestHeadersBuilder.append(userAgentString);
317 // Set the `Upgrade-Insecure-Requests` header property.
318 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
320 // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
321 requestHeadersBuilder.append(System.getProperty("line.separator"));
322 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
323 requestHeadersBuilder.append("Upgrade-Insecure-Requests", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
324 } else { // Older versions not so much.
325 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
326 requestHeadersBuilder.append("Upgrade-Insecure_Requests");
327 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
328 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
330 requestHeadersBuilder.append(": 1");
333 // Set the `x-requested-with` header property.
334 httpUrlConnection.setRequestProperty("x-requested-with", "");
336 // Add the `x-requested-with` header to the string builder and format the text.
337 requestHeadersBuilder.append(System.getProperty("line.separator"));
338 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
339 requestHeadersBuilder.append("x-requested-with", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
340 } else { // Older versions not so much.
341 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
342 requestHeadersBuilder.append("x-requested-with");
343 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
344 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
346 requestHeadersBuilder.append(": ");
349 // Get a handle for the shared preferences.
350 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
352 // Only populate `Do Not Track` if it is enabled.
353 if (sharedPreferences.getBoolean("do_not_track", false)) {
354 // Set the `dnt` header property.
355 httpUrlConnection.setRequestProperty("dnt", "1");
357 // Add the `dnt` header to the string builder and format the text.
358 requestHeadersBuilder.append(System.getProperty("line.separator"));
359 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
360 requestHeadersBuilder.append("dnt", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
361 } else { // Older versions not so much.
362 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
363 requestHeadersBuilder.append("dnt");
364 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
365 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
367 requestHeadersBuilder.append(": 1");
371 // Set the `Accept` header property.
372 httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
374 // Add the `Accept` header to the string builder and format the text.
375 requestHeadersBuilder.append(System.getProperty("line.separator"));
376 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
377 requestHeadersBuilder.append("Accept", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
378 } else { // Older versions not so much.
379 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
380 requestHeadersBuilder.append("Accept");
381 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
382 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
384 requestHeadersBuilder.append(": ");
385 requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
388 // Instantiate a locale string.
391 // Populate the locale string.
392 if (Build.VERSION.SDK_INT >= 24) { // SDK >= 24 has a list of locales.
393 // Get the list of locales.
394 LocaleList localeList = getResources().getConfiguration().getLocales();
396 // Initialize a string builder to extract the locales from the list.
397 StringBuilder localesStringBuilder = new StringBuilder();
399 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
402 // Populate the string builder with the contents of the locales list.
403 for (int i = 0; i < localeList.size(); i++) {
404 // Append a comma if there is already an item in the string builder.
406 localesStringBuilder.append(",");
409 // Get the indicated locale from the list.
410 localesStringBuilder.append(localeList.get(i));
412 // If not the first locale, append `;q=0.i`, which drops by .1 for each removal from the main locale.
414 localesStringBuilder.append(";q=0.");
415 localesStringBuilder.append(q);
422 // Store the populated string builder in the locale string.
423 localeString = localesStringBuilder.toString();
424 } else { // SDK < 24 only has a primary locale.
425 // Store the locale in the locale string.
426 localeString = Locale.getDefault().toString();
429 // Set the `Accept-Language` header property.
430 httpUrlConnection.setRequestProperty("Accept-Language", localeString);
432 // Add the `Accept-Language` header to the string builder and format the text.
433 requestHeadersBuilder.append(System.getProperty("line.separator"));
434 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
435 requestHeadersBuilder.append("Accept-Language", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
436 } else { // Older versions not so much.
437 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
438 requestHeadersBuilder.append("Accept-Language");
439 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
440 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
442 requestHeadersBuilder.append(": ");
443 requestHeadersBuilder.append(localeString);
446 // Get the cookies for the current domain.
447 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
449 // Only process the cookies if they are not null.
450 if (cookiesString != null) {
451 // Set the `Cookie` header property.
452 httpUrlConnection.setRequestProperty("Cookie", cookiesString);
454 // Add the `Cookie` header to the string builder and format the text.
455 requestHeadersBuilder.append(System.getProperty("line.separator"));
456 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
457 requestHeadersBuilder.append("Cookie", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
458 } else { // Older versions not so much.
459 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
460 requestHeadersBuilder.append("Cookie");
461 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
462 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
464 requestHeadersBuilder.append(": ");
465 requestHeadersBuilder.append(cookiesString);
469 // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default. If the property is manually set, than `HttpUrlConnection` does not process the decoding.
470 // Add the `Accept-Encoding` header to the string builder and format the text.
471 requestHeadersBuilder.append(System.getProperty("line.separator"));
472 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
473 requestHeadersBuilder.append("Accept-Encoding", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
474 } else { // Older versions not so much.
475 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
476 requestHeadersBuilder.append("Accept-Encoding");
477 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
478 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
480 requestHeadersBuilder.append(": gzip");
483 // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
485 // Initialize the string builders.
486 responseMessageBuilder = new SpannableStringBuilder();
487 responseHeadersBuilder = new SpannableStringBuilder();
489 // Get the response code, which causes the connection to the server to be made.
490 int responseCode = httpUrlConnection.getResponseCode();
492 // Populate the response message string builder.
493 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
494 responseMessageBuilder.append(String.valueOf(responseCode), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
495 } else { // Older versions not so much.
496 responseMessageBuilder.append(String.valueOf(responseCode));
497 int newLength = responseMessageBuilder.length();
498 responseMessageBuilder.setSpan(new StyleSpan(Typeface.BOLD), 0, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
500 responseMessageBuilder.append(": ");
501 responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
503 // Initialize the iteration variable.
506 // Iterate through the received header fields.
507 while (httpUrlConnection.getHeaderField(i) != null) {
508 // Add a new line if there is already information in the string builder.
510 responseHeadersBuilder.append(System.getProperty("line.separator"));
513 // Add the header to the string builder and format the text.
514 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
515 responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
516 } else { // Older versions not so much.
517 int oldLength = responseHeadersBuilder.length();
518 responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i));
519 int newLength = responseHeadersBuilder.length();
520 responseHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldLength + 1, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
522 responseHeadersBuilder.append(": ");
523 responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
525 // Increment the iteration variable.
529 // Instantiate an input stream for the response body.
530 InputStream inputStream;
532 // Get the correct input stream based on the response code.
533 if (responseCode == 404) { // Get the error stream.
534 inputStream = new BufferedInputStream(httpUrlConnection.getErrorStream());
535 } else { // Get the response body stream.
536 inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
539 // Initialize the byte array output stream and the conversion buffer byte array.
540 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
541 byte[] conversionBufferByteArray = new byte[1024];
543 // Instantiate the variable to track the buffer length.
547 // Attempt to read data from the input stream and store it in the conversion buffer byte array. Also store the amount of data transferred in the buffer length variable.
548 while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer is > 0.
549 // Write the contents of the conversion buffer to the byte array output stream.
550 byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength);
552 } catch (IOException e) {
556 // Close the input stream.
559 // Populate the response body string with the contents of the byte array output stream.
560 responseBodyString = byteArrayOutputStream.toString();
562 // Disconnect `httpUrlConnection`.
563 httpUrlConnection.disconnect();
565 } catch (IOException e) {
569 // Return the response body string as the result.
570 return responseBodyString;
573 // `onPostExecute()` operates on the UI thread.
575 protected void onPostExecute(String responseBodyString){
576 // Get handles for the text views.
577 TextView requestHeadersTextView = findViewById(R.id.request_headers);
578 TextView responseMessageTextView = findViewById(R.id.response_message);
579 TextView responseHeadersTextView = findViewById(R.id.response_headers);
580 TextView responseBodyTextView = findViewById(R.id.response_body);
581 ProgressBar progressBar = findViewById(R.id.progress_bar);
583 // Populate the text views.
584 requestHeadersTextView.setText(requestHeadersBuilder);
585 responseMessageTextView.setText(responseMessageBuilder);
586 responseHeadersTextView.setText(responseHeadersBuilder);
587 responseBodyTextView.setText(responseBodyString);
589 // Hide the progress bar.
590 progressBar.setIndeterminate(false);
591 progressBar.setVisibility(View.GONE);