Respect proxies when getting source and saving URLs. https://redmine.stoutner.com...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / asynctasks / GetSource.java
1 /*
2  * Copyright © 2017-2020 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.asynctasks;
21
22 import android.app.Activity;
23 import android.content.Context;
24 import android.content.SharedPreferences;
25 import android.graphics.Typeface;
26 import android.os.AsyncTask;
27 import android.os.Build;
28 import android.os.LocaleList;
29 import android.preference.PreferenceManager;
30 import android.text.SpannableStringBuilder;
31 import android.text.Spanned;
32 import android.text.style.StyleSpan;
33 import android.view.View;
34 import android.webkit.CookieManager;
35 import android.widget.ProgressBar;
36 import android.widget.TextView;
37
38 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
39
40 import com.stoutner.privacybrowser.R;
41 import com.stoutner.privacybrowser.helpers.ProxyHelper;
42
43 import java.io.BufferedInputStream;
44 import java.io.ByteArrayOutputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.lang.ref.WeakReference;
48 import java.net.HttpURLConnection;
49 import java.net.Proxy;
50 import java.net.URL;
51 import java.util.Locale;
52
53 // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `SpannableStringBuilder[]` contains the results.
54 public class GetSource extends AsyncTask<String, Void, SpannableStringBuilder[]> {
55     // Define weak references to the calling context and activity.
56     private WeakReference<Context> contextWeakReference;
57     private WeakReference<Activity> activityWeakReference;
58
59     // Store the user agent.
60     private String userAgent;
61
62     public GetSource(Context context, Activity activity, String userAgent) {
63         // Populate the weak references to the calling context and activity.
64         contextWeakReference = new WeakReference<>(context);
65         activityWeakReference = new WeakReference<>(activity);
66
67         // Store the user agent.
68         this.userAgent = userAgent;
69     }
70
71     // `onPreExecute()` operates on the UI thread.
72     @Override
73     protected void onPreExecute() {
74         // Get a handle for the calling activity.
75         Activity activity = activityWeakReference.get();
76
77         // Abort if the activity is gone.
78         if ((activity == null) || activity.isFinishing()) {
79             return;
80         }
81
82         // Get a handle for the progress bar.
83         ProgressBar progressBar = activity.findViewById(R.id.progress_bar);
84
85         // Make the progress bar visible.
86         progressBar.setVisibility(View.VISIBLE);
87
88         // Set the progress bar to be indeterminate.
89         progressBar.setIndeterminate(true);
90     }
91
92     @Override
93     protected SpannableStringBuilder[] doInBackground(String... formattedUrlString) {
94         // Initialize the response body String.
95         SpannableStringBuilder requestHeadersBuilder = new SpannableStringBuilder();
96         SpannableStringBuilder responseMessageBuilder = new SpannableStringBuilder();
97         SpannableStringBuilder responseHeadersBuilder = new SpannableStringBuilder();
98         SpannableStringBuilder responseBodyBuilder = new SpannableStringBuilder();
99
100         // Get a handle for the context and activity.
101         Context context = contextWeakReference.get();
102         Activity activity = activityWeakReference.get();
103
104         // Abort if the activity is gone.
105         if ((activity == null) || activity.isFinishing()) {
106             return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
107         }
108
109         // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
110         try {
111             // Get the current URL from the main activity.
112             URL url = new URL(formattedUrlString[0]);
113
114             // Instantiate the proxy helper.
115             ProxyHelper proxyHelper = new ProxyHelper();
116
117             // Get the current proxy.
118             Proxy proxy = proxyHelper.getCurrentProxy(context);
119
120             // Open a connection to the URL.  No data is actually sent at this point.
121             HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy);
122
123             // Define the variables necessary to build the request headers.
124             requestHeadersBuilder = new SpannableStringBuilder();
125             int oldRequestHeadersBuilderLength;
126             int newRequestHeadersBuilderLength;
127
128
129             // Set the `Host` header property.
130             httpUrlConnection.setRequestProperty("Host", url.getHost());
131
132             // Add the `Host` header to the string builder and format the text.
133             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
134                 requestHeadersBuilder.append("Host", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
135             } else {  // Older versions not so much.
136                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
137                 requestHeadersBuilder.append("Host");
138                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
139                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
140             }
141             requestHeadersBuilder.append(":  ");
142             requestHeadersBuilder.append(url.getHost());
143
144
145             // Set the `Connection` header property.
146             httpUrlConnection.setRequestProperty("Connection", "keep-alive");
147
148             // Add the `Connection` header to the string builder and format the text.
149             requestHeadersBuilder.append(System.getProperty("line.separator"));
150             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
151                 requestHeadersBuilder.append("Connection", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
152             } else {  // Older versions not so much.
153                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
154                 requestHeadersBuilder.append("Connection");
155                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
156                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
157             }
158             requestHeadersBuilder.append(":  keep-alive");
159
160
161             // Set the `Upgrade-Insecure-Requests` header property.
162             httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
163
164             // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
165             requestHeadersBuilder.append(System.getProperty("line.separator"));
166             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
167                 requestHeadersBuilder.append("Upgrade-Insecure-Requests", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
168             } else {  // Older versions not so much.
169                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
170                 requestHeadersBuilder.append("Upgrade-Insecure_Requests");
171                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
172                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
173             }
174             requestHeadersBuilder.append(":  1");
175
176
177             // Set the `User-Agent` header property.
178             httpUrlConnection.setRequestProperty("User-Agent", userAgent);
179
180             // Add the `User-Agent` header to the string builder and format the text.
181             requestHeadersBuilder.append(System.getProperty("line.separator"));
182             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
183                 requestHeadersBuilder.append("User-Agent", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
184             } else {  // Older versions not so much.
185                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
186                 requestHeadersBuilder.append("User-Agent");
187                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
188                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
189             }
190             requestHeadersBuilder.append(":  ");
191             requestHeadersBuilder.append(userAgent);
192
193
194             // Set the `x-requested-with` header property.
195             httpUrlConnection.setRequestProperty("x-requested-with", "");
196
197             // Add the `x-requested-with` header to the string builder and format the text.
198             requestHeadersBuilder.append(System.getProperty("line.separator"));
199             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
200                 requestHeadersBuilder.append("x-requested-with", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
201             } else {  // Older versions not so much.
202                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
203                 requestHeadersBuilder.append("x-requested-with");
204                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
205                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
206             }
207             requestHeadersBuilder.append(":  ");
208
209
210             // Set the `Sec-Fetch-Site` header property.
211             httpUrlConnection.setRequestProperty("Sec-Fetch-Site", "none");
212
213             // Add the `Sec-Fetch-Site` header to the string builder and format the text.
214             requestHeadersBuilder.append(System.getProperty("line.separator"));
215             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
216                 requestHeadersBuilder.append("Sec-Fetch-Site", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
217             } else {  // Older versions not so much.
218                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
219                 requestHeadersBuilder.append("Sec-Fetch-Site");
220                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
221                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
222             }
223             requestHeadersBuilder.append(":  none");
224
225
226             // Set the `Sec-Fetch-Mode` header property.
227             httpUrlConnection.setRequestProperty("Sec-Fetch-Mode", "navigate");
228
229             // Add the `Sec-Fetch-Mode` header to the string builder and format the text.
230             requestHeadersBuilder.append(System.getProperty("line.separator"));
231             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
232                 requestHeadersBuilder.append("Sec-Fetch-Mode", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
233             } else {  // Older versions not so much.
234                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
235                 requestHeadersBuilder.append("Sec-Fetch-Mode");
236                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
237                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
238             }
239             requestHeadersBuilder.append(":  navigate");
240
241
242             // Set the `Sec-Fetch-User` header property.
243             httpUrlConnection.setRequestProperty("Sec-Fetch-User", "?1");
244
245             // Add the `Sec-Fetch-User` header to the string builder and format the text.
246             requestHeadersBuilder.append(System.getProperty("line.separator"));
247             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
248                 requestHeadersBuilder.append("Sec-Fetch-User", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
249             } else {  // Older versions not so much.
250                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
251                 requestHeadersBuilder.append("Sec-Fetch-User");
252                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
253                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
254             }
255             requestHeadersBuilder.append(":  ?1");
256
257
258             // Get a handle for the shared preferences.
259             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
260
261             // Only populate `Do Not Track` if it is enabled.
262             if (sharedPreferences.getBoolean("do_not_track", false)) {
263                 // Set the `dnt` header property.
264                 httpUrlConnection.setRequestProperty("dnt", "1");
265
266                 // Add the `dnt` header to the string builder and format the text.
267                 requestHeadersBuilder.append(System.getProperty("line.separator"));
268                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
269                     requestHeadersBuilder.append("dnt", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
270                 } else {  // Older versions not so much.
271                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
272                     requestHeadersBuilder.append("dnt");
273                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
274                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
275                 }
276                 requestHeadersBuilder.append(":  1");
277             }
278
279
280             // Set the `Accept` header property.
281             httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
282
283             // Add the `Accept` header to the string builder and format the text.
284             requestHeadersBuilder.append(System.getProperty("line.separator"));
285             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
286                 requestHeadersBuilder.append("Accept", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
287             } else {  // Older versions not so much.
288                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
289                 requestHeadersBuilder.append("Accept");
290                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
291                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
292             }
293             requestHeadersBuilder.append(":  ");
294             requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
295
296
297             // Instantiate a locale string.
298             String localeString;
299
300             // Populate the locale string.
301             if (Build.VERSION.SDK_INT >= 24) {  // SDK >= 24 has a list of locales.
302                 // Get the list of locales.
303                 LocaleList localeList = activity.getResources().getConfiguration().getLocales();
304
305                 // Initialize a string builder to extract the locales from the list.
306                 StringBuilder localesStringBuilder = new StringBuilder();
307
308                 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
309                 int q = 10;
310
311                 // Populate the string builder with the contents of the locales list.
312                 for (int i = 0; i < localeList.size(); i++) {
313                     // Append a comma if there is already an item in the string builder.
314                     if (i > 0) {
315                         localesStringBuilder.append(",");
316                     }
317
318                     // Get the locale from the list.
319                     Locale locale = localeList.get(i);
320
321                     // Add the locale to the string.  `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
322                     localesStringBuilder.append(locale.getLanguage());
323                     localesStringBuilder.append("-");
324                     localesStringBuilder.append(locale.getCountry());
325
326                     // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
327                     if (q < 10) {
328                         localesStringBuilder.append(";q=0.");
329                         localesStringBuilder.append(q);
330                     }
331
332                     // Decrement `q` if it is greater than 1.
333                     if (q > 1) {
334                         q--;
335                     }
336
337                     // Add a second entry for the language only portion of the locale.
338                     localesStringBuilder.append(",");
339                     localesStringBuilder.append(locale.getLanguage());
340
341                     // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
342                     localesStringBuilder.append(";q=0.");
343                     localesStringBuilder.append(q);
344
345                     // Decrement `q` if it is greater than 1.
346                     if (q > 1) {
347                         q--;
348                     }
349                 }
350
351                 // Store the populated string builder in the locale string.
352                 localeString = localesStringBuilder.toString();
353             } else {  // SDK < 24 only has a primary locale.
354                 // Store the locale in the locale string.
355                 localeString = Locale.getDefault().toString();
356             }
357
358             // Set the `Accept-Language` header property.
359             httpUrlConnection.setRequestProperty("Accept-Language", localeString);
360
361             // Add the `Accept-Language` header to the string builder and format the text.
362             requestHeadersBuilder.append(System.getProperty("line.separator"));
363             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
364                 requestHeadersBuilder.append("Accept-Language", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
365             } else {  // Older versions not so much.
366                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
367                 requestHeadersBuilder.append("Accept-Language");
368                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
369                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
370             }
371             requestHeadersBuilder.append(":  ");
372             requestHeadersBuilder.append(localeString);
373
374
375             // Get the cookies for the current domain.
376             String cookiesString = CookieManager.getInstance().getCookie(url.toString());
377
378             // Only process the cookies if they are not null.
379             if (cookiesString != null) {
380                 // Add the cookies to the header property.
381                 httpUrlConnection.setRequestProperty("Cookie", cookiesString);
382
383                 // Add the cookie header to the string builder and format the text.
384                 requestHeadersBuilder.append(System.getProperty("line.separator"));
385                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
386                     requestHeadersBuilder.append("Cookie", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
387                 } else {  // Older versions not so much.
388                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
389                     requestHeadersBuilder.append("Cookie");
390                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
391                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
392                 }
393                 requestHeadersBuilder.append(":  ");
394                 requestHeadersBuilder.append(cookiesString);
395             }
396
397
398             // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default.  If the property is manually set, than `HttpUrlConnection` does not process the decoding.
399             // Add the `Accept-Encoding` header to the string builder and format the text.
400             requestHeadersBuilder.append(System.getProperty("line.separator"));
401             if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
402                 requestHeadersBuilder.append("Accept-Encoding", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
403             } else {  // Older versions not so much.
404                 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
405                 requestHeadersBuilder.append("Accept-Encoding");
406                 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
407                 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
408             }
409             requestHeadersBuilder.append(":  gzip");
410
411
412             // 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.
413             try {
414                 // Initialize the string builders.
415                 responseMessageBuilder = new SpannableStringBuilder();
416                 responseHeadersBuilder = new SpannableStringBuilder();
417
418                 // Get the response code, which causes the connection to the server to be made.
419                 int responseCode = httpUrlConnection.getResponseCode();
420
421                 // Populate the response message string builder.
422                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
423                     responseMessageBuilder.append(String.valueOf(responseCode), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
424                 } else {  // Older versions not so much.
425                     responseMessageBuilder.append(String.valueOf(responseCode));
426                     int newLength = responseMessageBuilder.length();
427                     responseMessageBuilder.setSpan(new StyleSpan(Typeface.BOLD), 0, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
428                 }
429                 responseMessageBuilder.append(":  ");
430                 responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
431
432                 // Initialize the iteration variable.
433                 int i = 0;
434
435                 // Iterate through the received header fields.
436                 while (httpUrlConnection.getHeaderField(i) != null) {
437                     // Add a new line if there is already information in the string builder.
438                     if (i > 0) {
439                         responseHeadersBuilder.append(System.getProperty("line.separator"));
440                     }
441
442                     // Add the header to the string builder and format the text.
443                     if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
444                         responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
445                     } else {  // Older versions not so much.
446                         int oldLength = responseHeadersBuilder.length();
447                         responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i));
448                         int newLength = responseHeadersBuilder.length();
449                         responseHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldLength, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
450                     }
451                     responseHeadersBuilder.append(":  ");
452                     responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
453
454                     // Increment the iteration variable.
455                     i++;
456                 }
457
458                 // Instantiate an input stream for the response body.
459                 InputStream inputStream;
460
461                 // Get the correct input stream based on the response code.
462                 if (responseCode == 404) {  // Get the error stream.
463                     inputStream = new BufferedInputStream(httpUrlConnection.getErrorStream());
464                 } else {  // Get the response body stream.
465                     inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
466                 }
467
468                 // Initialize the byte array output stream and the conversion buffer byte array.
469                 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
470                 byte[] conversionBufferByteArray = new byte[1024];
471
472                 // Define the buffer length variable.
473                 int bufferLength;
474
475                 try {
476                     // Attempt to read data from the input stream and store it in the conversion buffer byte array.  Also store the amount of data read in the buffer length variable.
477                     while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) {  // Proceed while the amount of data stored in the buffer is > 0.
478                         // Write the contents of the conversion buffer to the byte array output stream.
479                         byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength);
480                     }
481                 } catch (IOException exception) {
482                     // Do nothing.
483                 }
484
485                 // Close the input stream.
486                 inputStream.close();
487
488                 // Populate the response body string with the contents of the byte array output stream.
489                 responseBodyBuilder.append(byteArrayOutputStream.toString());
490             } finally {
491                 // Disconnect HTTP URL connection.
492                 httpUrlConnection.disconnect();
493             }
494         } catch (IOException exception) {
495             exception.printStackTrace();
496         }
497
498         // Return the response body string as the result.
499         return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
500     }
501
502     // `onPostExecute()` operates on the UI thread.
503     @Override
504     protected void onPostExecute(SpannableStringBuilder[] viewSourceStringArray){
505         // Get a handle for the activity.
506         Activity activity = activityWeakReference.get();
507
508         // Abort if the activity is gone.
509         if ((activity == null) || activity.isFinishing()) {
510             return;
511         }
512
513         // Get handles for the text views.
514         TextView requestHeadersTextView = activity.findViewById(R.id.request_headers);
515         TextView responseMessageTextView = activity.findViewById(R.id.response_message);
516         TextView responseHeadersTextView = activity.findViewById(R.id.response_headers);
517         TextView responseBodyTextView = activity.findViewById(R.id.response_body);
518         ProgressBar progressBar = activity.findViewById(R.id.progress_bar);
519         SwipeRefreshLayout swipeRefreshLayout = activity.findViewById(R.id.view_source_swiperefreshlayout);
520
521         // Populate the text views.  This can take a long time, and freeze the user interface, if the response body is particularly large.
522         requestHeadersTextView.setText(viewSourceStringArray[0]);
523         responseMessageTextView.setText(viewSourceStringArray[1]);
524         responseHeadersTextView.setText(viewSourceStringArray[2]);
525         responseBodyTextView.setText(viewSourceStringArray[3]);
526
527         // Hide the progress bar.
528         progressBar.setIndeterminate(false);
529         progressBar.setVisibility(View.GONE);
530
531         //Stop the swipe to refresh indicator if it is running
532         swipeRefreshLayout.setRefreshing(false);
533     }
534 }