2 * Copyright © 2017-2019 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.activities;
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;
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;
53 import com.stoutner.privacybrowser.R;
54 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog;
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;
63 import java.util.Locale;
65 public class ViewSourceActivity extends AppCompatActivity {
66 // `activity` is used in `onCreate()` and `goBack()`.
67 private Activity activity;
69 // The color spans are used in `onCreate()` and `highlightUrlText()`.
70 private ForegroundColorSpan redColorSpan;
71 private ForegroundColorSpan initialGrayColorSpan;
72 private ForegroundColorSpan finalGrayColorSpan;
75 protected void onCreate(Bundle savedInstanceState) {
76 // Disable screenshots if not allowed.
77 if (!MainWebViewActivity.allowScreenshots) {
78 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
82 if (MainWebViewActivity.darkTheme) {
83 setTheme(R.style.PrivacyBrowserDark);
85 setTheme(R.style.PrivacyBrowserLight);
88 // Run the default commands.
89 super.onCreate(savedInstanceState);
91 // Store a handle for the current activity.
94 // Set the content view.
95 setContentView(R.layout.view_source_coordinatorlayout);
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);
101 // Get a handle for the action bar.
102 final ActionBar actionBar = getSupportActionBar();
104 // Remove the incorrect lint warning that the action bar might be null.
105 assert actionBar != null;
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);
111 // Get a handle for the url text box.
112 EditText urlEditText = findViewById(R.id.url_edittext);
114 // Get the formatted URL string from the main activity.
115 String formattedUrlString = MainWebViewActivity.formattedUrlString;
117 // Populate the URL text box.
118 urlEditText.setText(formattedUrlString);
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));
125 // Apply text highlighting to the URL.
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);
131 // Remove the lint warning that the input method manager might be null.
132 assert inputMethodManager != null;
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);
145 // Move to the beginning of the string.
146 urlEditText.setSelection(0);
148 // Reapply the highlighting.
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);
160 // Remove the focus from the URL box.
161 urlEditText.clearFocus();
164 String url = urlEditText.getText().toString();
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);
171 // Consume the key press.
174 // Do not consume the key press.
179 // Implement swipe to refresh.
180 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.view_source_swiperefreshlayout);
181 swipeRefreshLayout.setOnRefreshListener(() -> {
183 String url = urlEditText.getText().toString();
185 // Get new source data for the URL if it begins with `http`.
186 if (url.startsWith("http")) {
187 new GetSource(this).execute(url);
189 // Stop the refresh animation.
190 swipeRefreshLayout.setRefreshing(false);
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);
199 swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
202 // Get the source using an AsyncTask if the URL begins with `http`.
203 if (formattedUrlString.startsWith("http")) {
204 new GetSource(this).execute(formattedUrlString);
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);
218 public boolean onOptionsItemSelected(MenuItem menuItem) {
219 // Get a handle for the about alert dialog.
220 DialogFragment aboutDialogFragment = new AboutViewSourceDialog();
222 // Show the about alert dialog.
223 aboutDialogFragment.show(getFragmentManager(), getString(R.string.about));
225 // Consume the event.
229 public void goBack(View view) {
231 NavUtils.navigateUpFromSameTask(activity);
234 private void highlightUrlText() {
235 // Get a handle for the URL EditText.
236 EditText urlEditText = findViewById(R.id.url_edittext);
238 // Get the URL string.
239 String urlString = urlEditText.getText().toString();
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));
252 // Create a base URL string.
256 if (endOfDomainName > 0) { // There is at least one character after 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.
264 // Get the index of the last `.` in the domain.
265 int lastDotIndex = baseUrl.lastIndexOf(".");
267 // Get the index of the penultimate `.` in the domain.
268 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
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);
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);
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);
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);
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;
300 // Populate the weak reference to the calling activity.
301 GetSource(Activity activity) {
302 activityWeakReference = new WeakReference<>(activity);
305 // `onPreExecute()` operates on the UI thread.
307 protected void onPreExecute() {
308 // Get a handle for the activity.
309 Activity viewSourceActivity = activityWeakReference.get();
311 // Abort if the activity is gone.
312 if ((viewSourceActivity == null) || viewSourceActivity.isFinishing()) {
316 // Get a handle for the progress bar.
317 ProgressBar progressBar = viewSourceActivity.findViewById(R.id.progress_bar);
319 // Make the progress bar visible.
320 progressBar.setVisibility(View.VISIBLE);
322 // Set the progress bar to be indeterminate.
323 progressBar.setIndeterminate(true);
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();
334 // Get a handle for the activity.
335 Activity activity = activityWeakReference.get();
337 // Abort if the activity is gone.
338 if ((activity == null) || activity.isFinishing()) {
339 return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
342 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
344 // Get the current URL from the main activity.
345 URL url = new URL(formattedUrlString[0]);
347 // Open a connection to the URL. No data is actually sent at this point.
348 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
350 // Instantiate the variables necessary to build the request headers.
351 requestHeadersBuilder = new SpannableStringBuilder();
352 int oldRequestHeadersBuilderLength;
353 int newRequestHeadersBuilderLength;
356 // Set the `Host` header property.
357 httpUrlConnection.setRequestProperty("Host", url.getHost());
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);
368 requestHeadersBuilder.append(": ");
369 requestHeadersBuilder.append(url.getHost());
372 // Set the `Connection` header property.
373 httpUrlConnection.setRequestProperty("Connection", "keep-alive");
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);
385 requestHeadersBuilder.append(": keep-alive");
388 // Get the current `User-Agent` string.
389 String userAgentString = MainWebViewActivity.appliedUserAgentString;
391 // Set the `User-Agent` header property.
392 httpUrlConnection.setRequestProperty("User-Agent", userAgentString);
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);
404 requestHeadersBuilder.append(": ");
405 requestHeadersBuilder.append(userAgentString);
408 // Set the `Upgrade-Insecure-Requests` header property.
409 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
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);
421 requestHeadersBuilder.append(": 1");
424 // Set the `x-requested-with` header property.
425 httpUrlConnection.setRequestProperty("x-requested-with", "");
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);
437 requestHeadersBuilder.append(": ");
440 // Get a handle for the shared preferences.
441 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
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");
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);
458 requestHeadersBuilder.append(": 1");
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");
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);
475 requestHeadersBuilder.append(": ");
476 requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
479 // Instantiate a locale string.
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();
487 // Initialize a string builder to extract the locales from the list.
488 StringBuilder localesStringBuilder = new StringBuilder();
490 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
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.
497 localesStringBuilder.append(",");
500 // Get the indicated locale from the list.
501 localesStringBuilder.append(localeList.get(i));
503 // If not the first locale, append `;q=0.i`, which drops by .1 for each removal from the main locale.
505 localesStringBuilder.append(";q=0.");
506 localesStringBuilder.append(q);
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();
520 // Set the `Accept-Language` header property.
521 httpUrlConnection.setRequestProperty("Accept-Language", localeString);
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);
533 requestHeadersBuilder.append(": ");
534 requestHeadersBuilder.append(localeString);
537 // Get the cookies for the current domain.
538 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
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);
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);
555 requestHeadersBuilder.append(": ");
556 requestHeadersBuilder.append(cookiesString);
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);
571 requestHeadersBuilder.append(": gzip");
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.
576 // Initialize the string builders.
577 responseMessageBuilder = new SpannableStringBuilder();
578 responseHeadersBuilder = new SpannableStringBuilder();
580 // Get the response code, which causes the connection to the server to be made.
581 int responseCode = httpUrlConnection.getResponseCode();
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);
591 responseMessageBuilder.append(": ");
592 responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
594 // Initialize the iteration variable.
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.
601 responseHeadersBuilder.append(System.getProperty("line.separator"));
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);
613 responseHeadersBuilder.append(": ");
614 responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
616 // Increment the iteration variable.
620 // Instantiate an input stream for the response body.
621 InputStream inputStream;
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());
630 // Initialize the byte array output stream and the conversion buffer byte array.
631 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
632 byte[] conversionBufferByteArray = new byte[1024];
634 // Instantiate the variable to track the buffer length.
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);
643 } catch (IOException e) {
647 // Close the input stream.
650 // Populate the response body string with the contents of the byte array output stream.
651 responseBodyBuilder.append(byteArrayOutputStream.toString());
653 // Disconnect `httpUrlConnection`.
654 httpUrlConnection.disconnect();
656 } catch (IOException e) {
660 // Return the response body string as the result.
661 return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
664 // `onPostExecute()` operates on the UI thread.
666 protected void onPostExecute(SpannableStringBuilder[] viewSourceStringArray){
667 // Get a handle for the activity.
668 Activity activity = activityWeakReference.get();
670 // Abort if the activity is gone.
671 if ((activity == null) || activity.isFinishing()) {
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);
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]);
689 // Hide the progress bar.
690 progressBar.setIndeterminate(false);
691 progressBar.setVisibility(View.GONE);
693 //Stop the swipe to refresh indicator if it is running
694 swipeRefreshLayout.setRefreshing(false);