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