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