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