Add a View Source activity. https://redmine.stoutner.com/issues/64
[PrivacyBrowser.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.inputmethod.InputMethodManager;
46 import android.webkit.CookieManager;
47 import android.widget.EditText;
48 import android.widget.ProgressBar;
49 import android.widget.TextView;
50
51 import com.stoutner.privacybrowser.R;
52 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog;
53
54 import java.io.BufferedInputStream;
55 import java.io.ByteArrayOutputStream;
56 import java.io.IOException;
57 import java.io.InputStream;
58 import java.net.HttpURLConnection;
59 import java.net.URL;
60 import java.util.Locale;
61
62 public class ViewSourceActivity extends AppCompatActivity {
63     // `activity` is used in `onCreate()` and `goBack()`.
64     Activity activity;
65
66     // The color spans are used in `onCreate()` and `highlightUrlText()`.
67     private ForegroundColorSpan redColorSpan;
68     private ForegroundColorSpan initialGrayColorSpan;
69     private ForegroundColorSpan finalGrayColorSpan;
70
71     @Override
72     protected void onCreate(Bundle savedInstanceState) {
73         // Set the theme.
74         if (MainWebViewActivity.darkTheme) {
75             setTheme(R.style.PrivacyBrowserDark);
76         } else {
77             setTheme(R.style.PrivacyBrowserLight);
78         }
79
80         // Run the default commands.
81         super.onCreate(savedInstanceState);
82
83         // Store a handle for the current activity.
84         activity = this;
85
86         // Set the content view.
87         setContentView(R.layout.view_source_coordinatorlayout);
88
89         // `SupportActionBar` from `android.support.v7.app.ActionBar` must be used until the minimum API is >= 21.
90         Toolbar viewSourceAppBar = findViewById(R.id.view_source_toolbar);
91         setSupportActionBar(viewSourceAppBar);
92
93         // Setup the app bar.
94         final ActionBar appBar = getSupportActionBar();
95
96         // Remove the incorrect warning in Android Studio that appBar might be null.
97         assert appBar != null;
98
99         // Add the custom layout to the app bar.
100         appBar.setCustomView(R.layout.view_source_app_bar);
101         appBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
102
103         // Get a handle for the url text box.
104         EditText urlEditText = findViewById(R.id.url_edittext);
105
106         // Get the formatted URL string from the main activity.
107         String formattedUrlString = MainWebViewActivity.formattedUrlString;
108
109         // Populate the URL text box.
110         urlEditText.setText(formattedUrlString);
111
112         // Initialize the foreground color spans for highlighting the URLs.  We have to use the deprecated `getColor()` until API >= 23.
113         redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
114         initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
115         finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500));
116
117         // Apply text highlighting to the URL.
118         highlightUrlText();
119
120         // Get a handle for the input method manager, which is used to hide the keyboard.
121         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
122
123         // Let Android Studio know that we aren't worried about the input method manager being null.
124         assert inputMethodManager != null;
125
126         // Remove the formatting from the URL when the user is editing the text.
127         urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
128             if (hasFocus) {  // The user is editing `urlTextBox`.
129                 // Remove the highlighting.
130                 urlEditText.getText().removeSpan(redColorSpan);
131                 urlEditText.getText().removeSpan(initialGrayColorSpan);
132                 urlEditText.getText().removeSpan(finalGrayColorSpan);
133             } else {  // The user has stopped editing `urlTextBox`.
134                 // Hide the soft keyboard.
135                 inputMethodManager.hideSoftInputFromWindow(urlEditText.getWindowToken(), 0);
136
137                 // Reapply the highlighting.
138                 highlightUrlText();
139
140
141             }
142         });
143
144         // Set the go button on the keyboard to request new source data.
145         urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
146             // Request new source data if the enter key was pressed.
147             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
148                 // Hide the soft keyboard.
149                 inputMethodManager.hideSoftInputFromWindow(urlEditText.getWindowToken(), 0);
150
151                 // Remove the focus from the URL box.
152                 urlEditText.clearFocus();
153
154                 // Get new source data for the current URL.
155                 new GetSource().execute(urlEditText.getText().toString());
156
157                 // Consume the key press.
158                 return true;
159             } else {
160                 // Do not consume the key press.
161                 return false;
162             }
163         });
164
165         // Get the source as an `AsyncTask`.
166         new GetSource().execute(formattedUrlString);
167     }
168
169     @Override
170     public boolean onCreateOptionsMenu(Menu menu) {
171         // Inflate the menu; this adds items to the action bar if it is present.
172         getMenuInflater().inflate(R.menu.view_source_options_menu, menu);
173
174         // Display the menu.
175         return true;
176     }
177
178     @Override
179     public boolean onOptionsItemSelected(MenuItem menuItem) {
180         // Get a handle for the about alert dialog.
181         DialogFragment aboutDialogFragment = new AboutViewSourceDialog();
182
183         // Show the about alert dialog.
184         aboutDialogFragment.show(getFragmentManager(), getString(R.string.about));
185
186         // Consume the event.
187         return true;
188     }
189
190     public void goBack(View view) {
191         // Go home.
192         NavUtils.navigateUpFromSameTask(activity);
193     }
194
195     private void highlightUrlText() {
196         // Get a handle for the URL EditText.
197         EditText urlEditText = findViewById(R.id.url_edittext);
198
199         // Get the URL.
200         String urlString = urlEditText.getText().toString();
201
202         // Highlight the beginning of the URL.
203         if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
204             urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
205         } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
206             urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
207         }
208
209         // Get the index of the `/` immediately after the domain name.
210         int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
211
212         // De-emphasize the text after the domain name.
213         if (endOfDomainName > 0) {
214             urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
215         }
216     }
217
218     // The first `String` declares the parameters.  The `Void` does not declare progress units.  The last `String` contains the results.
219     // `StaticFieldLeaks` are suppressed so that Android Studio doesn't complain about running an AsyncTask in a non-static context.
220     @SuppressLint("StaticFieldLeak")
221     private class GetSource extends AsyncTask<String, Void, String> {
222         // The class variables pass information from `doInBackground()` to `onPostExecute()`.
223         SpannableStringBuilder responseMessageBuilder;
224         SpannableStringBuilder requestHeadersBuilder;
225         SpannableStringBuilder responseHeadersBuilder;
226
227         // `onPreExecute()` operates on the UI thread.
228         @Override
229         protected void onPreExecute() {
230             // Get a handle for the progress bar.
231             ProgressBar progressBar = findViewById(R.id.progress_bar);
232
233             // Make the progress bar visible.
234             progressBar.setVisibility(View.VISIBLE);
235
236             // Set the progress bar to be indeterminate.
237             progressBar.setIndeterminate(true);
238         }
239
240         @Override
241         protected String doInBackground(String... formattedUrlString) {
242             // Initialize the response body `String`.
243             String responseBodyString = "";
244
245             // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
246             try {
247                 // Get the current URL from the main activity.
248                 URL url = new URL(formattedUrlString[0]);
249
250                 // Open a connection to the URL.  No data is actually sent at this point.
251                 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
252
253                 // Instantiate the variables necessary to build the request headers.
254                 requestHeadersBuilder = new SpannableStringBuilder();
255                 int oldRequestHeadersBuilderLength;
256                 int newRequestHeadersBuilderLength;
257
258
259                 // Set the `Host` header property.
260                 httpUrlConnection.setRequestProperty("Host", url.getHost());
261
262                 // Add the `Host` header to the string builder and format the text.
263                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
264                     requestHeadersBuilder.append("Host", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
265                 } else {  // Older versions not so much.
266                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
267                     requestHeadersBuilder.append("Host");
268                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
269                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
270                 }
271                 requestHeadersBuilder.append(":  ");
272                 requestHeadersBuilder.append(url.getHost());
273
274
275                 // Set the `Connection` header property.
276                 httpUrlConnection.setRequestProperty("Connection", "keep-alive");
277
278                 // Add the `Connection` header to the string builder and format the text.
279                 requestHeadersBuilder.append(System.getProperty("line.separator"));
280                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
281                     requestHeadersBuilder.append("Connection", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
282                 } else {  // Older versions not so much.
283                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
284                     requestHeadersBuilder.append("Connection");
285                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
286                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
287                 }
288                 requestHeadersBuilder.append(":  keep-alive");
289
290
291                 // Get the current `User-Agent` string.
292                 String userAgentString = MainWebViewActivity.appliedUserAgentString;
293
294                 // Set the `User-Agent` header property.
295                 httpUrlConnection.setRequestProperty("User-Agent", userAgentString);
296
297                 // Add the `User-Agent` header to the string builder and format the text.
298                 requestHeadersBuilder.append(System.getProperty("line.separator"));
299                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
300                     requestHeadersBuilder.append("User-Agent", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
301                 } else {  // Older versions not so much.
302                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
303                     requestHeadersBuilder.append("User-Agent");
304                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
305                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
306                 }
307                 requestHeadersBuilder.append(":  ");
308                 requestHeadersBuilder.append(userAgentString);
309
310
311                 // Set the `Upgrade-Insecure-Requests` header property.
312                 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
313
314                 // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
315                 requestHeadersBuilder.append(System.getProperty("line.separator"));
316                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
317                     requestHeadersBuilder.append("Upgrade-Insecure-Requests", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
318                 } else {  // Older versions not so much.
319                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
320                     requestHeadersBuilder.append("Upgrade-Insecure_Requests");
321                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
322                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
323                 }
324                 requestHeadersBuilder.append(":  1");
325
326
327                 // Set the `x-requested-with` header property.
328                 httpUrlConnection.setRequestProperty("x-requested-with", "");
329
330                 // Add the `x-requested-with` header to the string builder and format the text.
331                 requestHeadersBuilder.append(System.getProperty("line.separator"));
332                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
333                     requestHeadersBuilder.append("x-requested-with", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
334                 } else {  // Older versions not so much.
335                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
336                     requestHeadersBuilder.append("x-requested-with");
337                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
338                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
339                 }
340                 requestHeadersBuilder.append(":  ");
341
342
343                 // Get a handle for the shared preferences.
344                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
345
346                 // Only populate `Do Not Track` if it is enabled.
347                 if (sharedPreferences.getBoolean("do_not_track", false)) {
348                     // Set the `dnt` header property.
349                     httpUrlConnection.setRequestProperty("dnt", "1");
350
351                     // Add the `dnt` header to the string builder and format the text.
352                     requestHeadersBuilder.append(System.getProperty("line.separator"));
353                     if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
354                         requestHeadersBuilder.append("dnt", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
355                     } else {  // Older versions not so much.
356                         oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
357                         requestHeadersBuilder.append("dnt");
358                         newRequestHeadersBuilderLength = requestHeadersBuilder.length();
359                         requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
360                     }
361                     requestHeadersBuilder.append(":  1");
362                 }
363
364
365                 // Set the `Accept` header property.
366                 httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
367
368                 // Add the `Accept` header to the string builder and format the text.
369                 requestHeadersBuilder.append(System.getProperty("line.separator"));
370                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
371                     requestHeadersBuilder.append("Accept", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
372                 } else {  // Older versions not so much.
373                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
374                     requestHeadersBuilder.append("Accept");
375                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
376                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
377                 }
378                 requestHeadersBuilder.append(":  ");
379                 requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
380
381
382                 // Instantiate a locale string.
383                 String localeString;
384
385                 // Populate the locale string.
386                 if (Build.VERSION.SDK_INT >= 24) {  // SDK >= 24 has a list of locales.
387                     // Get the list of locales.
388                     LocaleList localeList = getResources().getConfiguration().getLocales();
389
390                     // Initialize a string builder to extract the locales from the list.
391                     StringBuilder localesStringBuilder = new StringBuilder();
392
393                     // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
394                     int q = 10;
395
396                     // Populate the string builder with the contents of the locales list.
397                     for (int i = 0; i < localeList.size(); i++) {
398                         // Append a comma if there is already an item in the string builder.
399                         if (i > 0) {
400                             localesStringBuilder.append(",");
401                         }
402
403                         // Get the indicated locale from the list.
404                         localesStringBuilder.append(localeList.get(i));
405
406                         // If not the first locale, append `;q=0.i`, which drops by .1 for each removal from the main locale.
407                         if (q < 10) {
408                             localesStringBuilder.append(";q=0.");
409                             localesStringBuilder.append(q);
410                         }
411
412                         // Decrement `q`.
413                         q--;
414                     }
415
416                     // Store the populated string builder in the locale string.
417                     localeString = localesStringBuilder.toString();
418                 } else {  // SDK < 24 only has a primary locale.
419                     // Store the locale in the locale string.
420                     localeString = Locale.getDefault().toString();
421                 }
422
423                 // Set the `Accept-Language` header property.
424                 httpUrlConnection.setRequestProperty("Accept-Language", localeString);
425
426                 // Add the `Accept-Language` header to the string builder and format the text.
427                 requestHeadersBuilder.append(System.getProperty("line.separator"));
428                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
429                     requestHeadersBuilder.append("Accept-Language", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
430                 } else {  // Older versions not so much.
431                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
432                     requestHeadersBuilder.append("Accept-Language");
433                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
434                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
435                 }
436                 requestHeadersBuilder.append(":  ");
437                 requestHeadersBuilder.append(localeString);
438
439
440                 // Get the cookies for the current domain.
441                 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
442
443                 // Only process the cookies if they are not null.
444                 if (cookiesString != null) {
445                     // Set the `Cookie` header property.
446                     httpUrlConnection.setRequestProperty("Cookie", cookiesString);
447
448                     // Add the `Cookie` header to the string builder and format the text.
449                     requestHeadersBuilder.append(System.getProperty("line.separator"));
450                     if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
451                         requestHeadersBuilder.append("Cookie", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
452                     } else {  // Older versions not so much.
453                         oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
454                         requestHeadersBuilder.append("Cookie");
455                         newRequestHeadersBuilderLength = requestHeadersBuilder.length();
456                         requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
457                     }
458                     requestHeadersBuilder.append(":  ");
459                     requestHeadersBuilder.append(cookiesString);
460                 }
461
462
463                 // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default.  If the property is manually set, than `HttpUrlConnection` does not process the decoding.
464                 // Add the `Accept-Encoding` header to the string builder and format the text.
465                 requestHeadersBuilder.append(System.getProperty("line.separator"));
466                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
467                     requestHeadersBuilder.append("Accept-Encoding", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
468                 } else {  // Older versions not so much.
469                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
470                     requestHeadersBuilder.append("Accept-Encoding");
471                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
472                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
473                 }
474                 requestHeadersBuilder.append(":  gzip");
475
476
477                 // 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.
478                 try {
479                     // Initialize the string builders.
480                     responseMessageBuilder = new SpannableStringBuilder();
481                     responseHeadersBuilder = new SpannableStringBuilder();
482
483                     // Get the response code, which causes the connection to the server to be made.
484                     int responseCode = httpUrlConnection.getResponseCode();
485
486                     // Populate the response message string builder.
487                     if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
488                         responseMessageBuilder.append(String.valueOf(responseCode), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
489                     } else {  // Older versions not so much.
490                         responseMessageBuilder.append(String.valueOf(responseCode));
491                         int newLength = responseMessageBuilder.length();
492                         responseMessageBuilder.setSpan(new StyleSpan(Typeface.BOLD), 0, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
493                     }
494                     responseMessageBuilder.append(":  ");
495                     responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
496
497                     // Initialize the iteration variable.
498                     int i = 0;
499
500                     // Iterate through the received header fields.
501                     while (httpUrlConnection.getHeaderField(i) != null) {
502                         // Add a new line if there is already information in the string builder.
503                         if (i > 0) {
504                             responseHeadersBuilder.append(System.getProperty("line.separator"));
505                         }
506
507                         // Add the header to the string builder and format the text.
508                         if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
509                             responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
510                         } else {  // Older versions not so much.
511                             int oldLength = responseHeadersBuilder.length();
512                             responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i));
513                             int newLength = responseHeadersBuilder.length();
514                             responseHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldLength + 1, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
515                         }
516                         responseHeadersBuilder.append(":  ");
517                         responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
518
519                         // Increment the iteration variable.
520                         i++;
521                     }
522
523                     // Instantiate an input stream for the response body.
524                     InputStream inputStream;
525
526                     // Get the correct input stream based on the response code.
527                     if (responseCode == 404) {  // Get the error stream.
528                         inputStream = new BufferedInputStream(httpUrlConnection.getErrorStream());
529                     } else {  // Get the response body stream.
530                         inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
531                     }
532
533                     // Initialize the byte array output stream and the conversion buffer byte array.
534                     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
535                     byte[] conversionBufferByteArray = new byte[1024];
536
537                     // Instantiate the variable to track the buffer length.
538                     int bufferLength;
539
540                     try {
541                         // 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.
542                         while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) {  // Proceed while the amount of data stored in the buffer is > 0.
543                             // Write the contents of the conversion buffer to the byte array output stream.
544                             byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength);
545                         }
546                     } catch (IOException e) {
547                         e.printStackTrace();
548                     }
549
550                     // Close the input stream.
551                     inputStream.close();
552
553                     // Populate the response body string with the contents of the byte array output stream.
554                     responseBodyString = byteArrayOutputStream.toString();
555                 } finally {
556                     // Disconnect `httpUrlConnection`.
557                     httpUrlConnection.disconnect();
558                 }
559             } catch (IOException e) {
560                 e.printStackTrace();
561             }
562
563             // Return the response body string as the result.
564             return responseBodyString;
565         }
566
567         // `onPostExecute()` operates on the UI thread.
568         @Override
569         protected void onPostExecute(String responseBodyString){
570             // Get handles for the text views.
571             TextView requestHeadersTextView = findViewById(R.id.request_headers);
572             TextView responseMessageTextView = findViewById(R.id.response_message);
573             TextView responseHeadersTextView = findViewById(R.id.response_headers);
574             TextView responseBodyTextView = findViewById(R.id.response_body);
575             ProgressBar progressBar = findViewById(R.id.progress_bar);
576
577             // Populate the text views.
578             requestHeadersTextView.setText(requestHeadersBuilder);
579             responseMessageTextView.setText(responseMessageBuilder);
580             responseHeadersTextView.setText(responseHeadersBuilder);
581             responseBodyTextView.setText(responseBodyString);
582
583             // Hide the progress bar.
584             progressBar.setIndeterminate(false);
585             progressBar.setVisibility(View.GONE);
586         }
587     }
588 }