]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.java
Fix the ghosting of Clear Data. https://redmine.stoutner.com/issues/317
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ViewSourceActivity.java
1 /*
2  * Copyright © 2017-2018 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.activities;
21
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;
51
52 import com.stoutner.privacybrowser.R;
53 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog;
54
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;
60 import java.net.URL;
61 import java.util.Locale;
62
63 public class ViewSourceActivity extends AppCompatActivity {
64     // `activity` is used in `onCreate()` and `goBack()`.
65     private Activity activity;
66
67     // The color spans are used in `onCreate()` and `highlightUrlText()`.
68     private ForegroundColorSpan redColorSpan;
69     private ForegroundColorSpan initialGrayColorSpan;
70     private ForegroundColorSpan finalGrayColorSpan;
71
72     @Override
73     protected void onCreate(Bundle savedInstanceState) {
74         // Disable screenshots if not allowed.
75         if (!MainWebViewActivity.allowScreenshots) {
76             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
77         }
78
79         // Set the theme.
80         if (MainWebViewActivity.darkTheme) {
81             setTheme(R.style.PrivacyBrowserDark);
82         } else {
83             setTheme(R.style.PrivacyBrowserLight);
84         }
85
86         // Run the default commands.
87         super.onCreate(savedInstanceState);
88
89         // Store a handle for the current activity.
90         activity = this;
91
92         // Set the content view.
93         setContentView(R.layout.view_source_coordinatorlayout);
94
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);
98
99         // Setup the app bar.
100         final ActionBar appBar = getSupportActionBar();
101
102         // Remove the incorrect warning in Android Studio that appBar might be null.
103         assert appBar != null;
104
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);
108
109         // Get a handle for the url text box.
110         EditText urlEditText = findViewById(R.id.url_edittext);
111
112         // Get the formatted URL string from the main activity.
113         String formattedUrlString = MainWebViewActivity.formattedUrlString;
114
115         // Populate the URL text box.
116         urlEditText.setText(formattedUrlString);
117
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));
122
123         // Apply text highlighting to the URL.
124         highlightUrlText();
125
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);
128
129         // Let Android Studio know that we aren't worried about the input method manager being null.
130         assert inputMethodManager != null;
131
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);
142
143                 // Reapply the highlighting.
144                 highlightUrlText();
145
146
147             }
148         });
149
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);
156
157                 // Remove the focus from the URL box.
158                 urlEditText.clearFocus();
159
160                 // Get new source data for the current URL.
161                 new GetSource().execute(urlEditText.getText().toString());
162
163                 // Consume the key press.
164                 return true;
165             } else {
166                 // Do not consume the key press.
167                 return false;
168             }
169         });
170
171         // Get the source as an `AsyncTask`.
172         new GetSource().execute(formattedUrlString);
173     }
174
175     @Override
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);
179
180         // Display the menu.
181         return true;
182     }
183
184     @Override
185     public boolean onOptionsItemSelected(MenuItem menuItem) {
186         // Get a handle for the about alert dialog.
187         DialogFragment aboutDialogFragment = new AboutViewSourceDialog();
188
189         // Show the about alert dialog.
190         aboutDialogFragment.show(getFragmentManager(), getString(R.string.about));
191
192         // Consume the event.
193         return true;
194     }
195
196     public void goBack(View view) {
197         // Go home.
198         NavUtils.navigateUpFromSameTask(activity);
199     }
200
201     private void highlightUrlText() {
202         // Get a handle for the URL EditText.
203         EditText urlEditText = findViewById(R.id.url_edittext);
204
205         // Get the URL.
206         String urlString = urlEditText.getText().toString();
207
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);
213         }
214
215         // Get the index of the `/` immediately after the domain name.
216         int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
217
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);
221         }
222     }
223
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;
232
233         // `onPreExecute()` operates on the UI thread.
234         @Override
235         protected void onPreExecute() {
236             // Get a handle for the progress bar.
237             ProgressBar progressBar = findViewById(R.id.progress_bar);
238
239             // Make the progress bar visible.
240             progressBar.setVisibility(View.VISIBLE);
241
242             // Set the progress bar to be indeterminate.
243             progressBar.setIndeterminate(true);
244         }
245
246         @Override
247         protected String doInBackground(String... formattedUrlString) {
248             // Initialize the response body `String`.
249             String responseBodyString = "";
250
251             // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
252             try {
253                 // Get the current URL from the main activity.
254                 URL url = new URL(formattedUrlString[0]);
255
256                 // Open a connection to the URL.  No data is actually sent at this point.
257                 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
258
259                 // Instantiate the variables necessary to build the request headers.
260                 requestHeadersBuilder = new SpannableStringBuilder();
261                 int oldRequestHeadersBuilderLength;
262                 int newRequestHeadersBuilderLength;
263
264
265                 // Set the `Host` header property.
266                 httpUrlConnection.setRequestProperty("Host", url.getHost());
267
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);
276                 }
277                 requestHeadersBuilder.append(":  ");
278                 requestHeadersBuilder.append(url.getHost());
279
280
281                 // Set the `Connection` header property.
282                 httpUrlConnection.setRequestProperty("Connection", "keep-alive");
283
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);
293                 }
294                 requestHeadersBuilder.append(":  keep-alive");
295
296
297                 // Get the current `User-Agent` string.
298                 String userAgentString = MainWebViewActivity.appliedUserAgentString;
299
300                 // Set the `User-Agent` header property.
301                 httpUrlConnection.setRequestProperty("User-Agent", userAgentString);
302
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);
312                 }
313                 requestHeadersBuilder.append(":  ");
314                 requestHeadersBuilder.append(userAgentString);
315
316
317                 // Set the `Upgrade-Insecure-Requests` header property.
318                 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
319
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);
329                 }
330                 requestHeadersBuilder.append(":  1");
331
332
333                 // Set the `x-requested-with` header property.
334                 httpUrlConnection.setRequestProperty("x-requested-with", "");
335
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);
345                 }
346                 requestHeadersBuilder.append(":  ");
347
348
349                 // Get a handle for the shared preferences.
350                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
351
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");
356
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);
366                     }
367                     requestHeadersBuilder.append(":  1");
368                 }
369
370
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");
373
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);
383                 }
384                 requestHeadersBuilder.append(":  ");
385                 requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
386
387
388                 // Instantiate a locale string.
389                 String localeString;
390
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();
395
396                     // Initialize a string builder to extract the locales from the list.
397                     StringBuilder localesStringBuilder = new StringBuilder();
398
399                     // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
400                     int q = 10;
401
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.
405                         if (i > 0) {
406                             localesStringBuilder.append(",");
407                         }
408
409                         // Get the indicated locale from the list.
410                         localesStringBuilder.append(localeList.get(i));
411
412                         // If not the first locale, append `;q=0.i`, which drops by .1 for each removal from the main locale.
413                         if (q < 10) {
414                             localesStringBuilder.append(";q=0.");
415                             localesStringBuilder.append(q);
416                         }
417
418                         // Decrement `q`.
419                         q--;
420                     }
421
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();
427                 }
428
429                 // Set the `Accept-Language` header property.
430                 httpUrlConnection.setRequestProperty("Accept-Language", localeString);
431
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);
441                 }
442                 requestHeadersBuilder.append(":  ");
443                 requestHeadersBuilder.append(localeString);
444
445
446                 // Get the cookies for the current domain.
447                 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
448
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);
453
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);
463                     }
464                     requestHeadersBuilder.append(":  ");
465                     requestHeadersBuilder.append(cookiesString);
466                 }
467
468
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);
479                 }
480                 requestHeadersBuilder.append(":  gzip");
481
482
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.
484                 try {
485                     // Initialize the string builders.
486                     responseMessageBuilder = new SpannableStringBuilder();
487                     responseHeadersBuilder = new SpannableStringBuilder();
488
489                     // Get the response code, which causes the connection to the server to be made.
490                     int responseCode = httpUrlConnection.getResponseCode();
491
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);
499                     }
500                     responseMessageBuilder.append(":  ");
501                     responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
502
503                     // Initialize the iteration variable.
504                     int i = 0;
505
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.
509                         if (i > 0) {
510                             responseHeadersBuilder.append(System.getProperty("line.separator"));
511                         }
512
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);
521                         }
522                         responseHeadersBuilder.append(":  ");
523                         responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
524
525                         // Increment the iteration variable.
526                         i++;
527                     }
528
529                     // Instantiate an input stream for the response body.
530                     InputStream inputStream;
531
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());
537                     }
538
539                     // Initialize the byte array output stream and the conversion buffer byte array.
540                     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
541                     byte[] conversionBufferByteArray = new byte[1024];
542
543                     // Instantiate the variable to track the buffer length.
544                     int bufferLength;
545
546                     try {
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);
551                         }
552                     } catch (IOException e) {
553                         e.printStackTrace();
554                     }
555
556                     // Close the input stream.
557                     inputStream.close();
558
559                     // Populate the response body string with the contents of the byte array output stream.
560                     responseBodyString = byteArrayOutputStream.toString();
561                 } finally {
562                     // Disconnect `httpUrlConnection`.
563                     httpUrlConnection.disconnect();
564                 }
565             } catch (IOException e) {
566                 e.printStackTrace();
567             }
568
569             // Return the response body string as the result.
570             return responseBodyString;
571         }
572
573         // `onPostExecute()` operates on the UI thread.
574         @Override
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);
582
583             // Populate the text views.
584             requestHeadersTextView.setText(requestHeadersBuilder);
585             responseMessageTextView.setText(responseMessageBuilder);
586             responseHeadersTextView.setText(responseHeadersBuilder);
587             responseBodyTextView.setText(responseBodyString);
588
589             // Hide the progress bar.
590             progressBar.setIndeterminate(false);
591             progressBar.setVisibility(View.GONE);
592         }
593     }
594 }