2 * Copyright © 2017-2018 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.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;
52 import com.stoutner.privacybrowser.R;
53 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog;
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;
62 import java.util.Locale;
64 public class ViewSourceActivity extends AppCompatActivity {
65 // `activity` is used in `onCreate()` and `goBack()`.
66 private Activity activity;
68 // The color spans are used in `onCreate()` and `highlightUrlText()`.
69 private ForegroundColorSpan redColorSpan;
70 private ForegroundColorSpan initialGrayColorSpan;
71 private ForegroundColorSpan finalGrayColorSpan;
74 protected void onCreate(Bundle savedInstanceState) {
75 // Disable screenshots if not allowed.
76 if (!MainWebViewActivity.allowScreenshots) {
77 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
81 if (MainWebViewActivity.darkTheme) {
82 setTheme(R.style.PrivacyBrowserDark);
84 setTheme(R.style.PrivacyBrowserLight);
87 // Run the default commands.
88 super.onCreate(savedInstanceState);
90 // Store a handle for the current activity.
93 // Set the content view.
94 setContentView(R.layout.view_source_coordinatorlayout);
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);
100 // Setup the app bar.
101 final ActionBar appBar = getSupportActionBar();
103 // Remove the incorrect warning in Android Studio that appBar might be null.
104 assert appBar != null;
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);
110 // Get a handle for the url text box.
111 EditText urlEditText = findViewById(R.id.url_edittext);
113 // Get the formatted URL string from the main activity.
114 String formattedUrlString = MainWebViewActivity.formattedUrlString;
116 // Populate the URL text box.
117 urlEditText.setText(formattedUrlString);
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));
124 // Apply text highlighting to the URL.
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);
130 // Remove the lint warning that the input method manager might be null.
131 assert inputMethodManager != null;
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);
144 // Move to the beginning of the string.
145 urlEditText.setSelection(0);
147 // Reapply the highlighting.
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);
159 // Remove the focus from the URL box.
160 urlEditText.clearFocus();
163 String url = urlEditText.getText().toString();
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);
170 // Consume the key press.
173 // Do not consume the key press.
178 // Implement swipe to refresh.
179 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.view_source_swiperefreshlayout);
180 swipeRefreshLayout.setOnRefreshListener(() -> {
182 String url = urlEditText.getText().toString();
184 // Get new source data for the URL if it begins with `http`.
185 if (url.startsWith("http")) {
186 new GetSource(this).execute(url);
188 // Stop the refresh animation.
189 swipeRefreshLayout.setRefreshing(false);
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);
198 swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
201 // Get the source using an AsyncTask if the URL begins with `http`.
202 if (formattedUrlString.startsWith("http")) {
203 new GetSource(this).execute(formattedUrlString);
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);
217 public boolean onOptionsItemSelected(MenuItem menuItem) {
218 // Get a handle for the about alert dialog.
219 DialogFragment aboutDialogFragment = new AboutViewSourceDialog();
221 // Show the about alert dialog.
222 aboutDialogFragment.show(getFragmentManager(), getString(R.string.about));
224 // Consume the event.
228 public void goBack(View view) {
230 NavUtils.navigateUpFromSameTask(activity);
233 private void highlightUrlText() {
234 // Get a handle for the URL EditText.
235 EditText urlEditText = findViewById(R.id.url_edittext);
237 // Get the URL string.
238 String urlString = urlEditText.getText().toString();
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));
251 // Create a base URL string.
255 if (endOfDomainName > 0) { // There is at least one character after 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.
263 // Get the index of the last `.` in the domain.
264 int lastDotIndex = baseUrl.lastIndexOf(".");
266 // Get the index of the penultimate `.` in the domain.
267 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
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);
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);
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);
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);
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;
299 // Populate the weak reference to the calling activity.
300 GetSource(Activity activity) {
301 activityWeakReference = new WeakReference<>(activity);
304 // `onPreExecute()` operates on the UI thread.
306 protected void onPreExecute() {
307 // Get a handle for the activity.
308 Activity viewSourceActivity = activityWeakReference.get();
310 // Abort if the activity is gone.
311 if ((viewSourceActivity == null) || (viewSourceActivity.isFinishing())) {
315 // Get a handle for the progress bar.
316 ProgressBar progressBar = viewSourceActivity.findViewById(R.id.progress_bar);
318 // Make the progress bar visible.
319 progressBar.setVisibility(View.VISIBLE);
321 // Set the progress bar to be indeterminate.
322 progressBar.setIndeterminate(true);
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();
333 // Get a handle for the activity.
334 Activity activity = activityWeakReference.get();
336 // Abort if the activity is gone.
337 if ((activity == null) || (activity.isFinishing())) {
338 return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
341 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
343 // Get the current URL from the main activity.
344 URL url = new URL(formattedUrlString[0]);
346 // Open a connection to the URL. No data is actually sent at this point.
347 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
349 // Instantiate the variables necessary to build the request headers.
350 requestHeadersBuilder = new SpannableStringBuilder();
351 int oldRequestHeadersBuilderLength;
352 int newRequestHeadersBuilderLength;
355 // Set the `Host` header property.
356 httpUrlConnection.setRequestProperty("Host", url.getHost());
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);
367 requestHeadersBuilder.append(": ");
368 requestHeadersBuilder.append(url.getHost());
371 // Set the `Connection` header property.
372 httpUrlConnection.setRequestProperty("Connection", "keep-alive");
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);
384 requestHeadersBuilder.append(": keep-alive");
387 // Get the current `User-Agent` string.
388 String userAgentString = MainWebViewActivity.appliedUserAgentString;
390 // Set the `User-Agent` header property.
391 httpUrlConnection.setRequestProperty("User-Agent", userAgentString);
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);
403 requestHeadersBuilder.append(": ");
404 requestHeadersBuilder.append(userAgentString);
407 // Set the `Upgrade-Insecure-Requests` header property.
408 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
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);
420 requestHeadersBuilder.append(": 1");
423 // Set the `x-requested-with` header property.
424 httpUrlConnection.setRequestProperty("x-requested-with", "");
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);
436 requestHeadersBuilder.append(": ");
439 // Get a handle for the shared preferences.
440 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
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");
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);
457 requestHeadersBuilder.append(": 1");
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");
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);
474 requestHeadersBuilder.append(": ");
475 requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
478 // Instantiate a locale string.
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();
486 // Initialize a string builder to extract the locales from the list.
487 StringBuilder localesStringBuilder = new StringBuilder();
489 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
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.
496 localesStringBuilder.append(",");
499 // Get the indicated locale from the list.
500 localesStringBuilder.append(localeList.get(i));
502 // If not the first locale, append `;q=0.i`, which drops by .1 for each removal from the main locale.
504 localesStringBuilder.append(";q=0.");
505 localesStringBuilder.append(q);
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();
519 // Set the `Accept-Language` header property.
520 httpUrlConnection.setRequestProperty("Accept-Language", localeString);
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);
532 requestHeadersBuilder.append(": ");
533 requestHeadersBuilder.append(localeString);
536 // Get the cookies for the current domain.
537 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
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);
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);
554 requestHeadersBuilder.append(": ");
555 requestHeadersBuilder.append(cookiesString);
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);
570 requestHeadersBuilder.append(": gzip");
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.
575 // Initialize the string builders.
576 responseMessageBuilder = new SpannableStringBuilder();
577 responseHeadersBuilder = new SpannableStringBuilder();
579 // Get the response code, which causes the connection to the server to be made.
580 int responseCode = httpUrlConnection.getResponseCode();
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);
590 responseMessageBuilder.append(": ");
591 responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
593 // Initialize the iteration variable.
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.
600 responseHeadersBuilder.append(System.getProperty("line.separator"));
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);
612 responseHeadersBuilder.append(": ");
613 responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
615 // Increment the iteration variable.
619 // Instantiate an input stream for the response body.
620 InputStream inputStream;
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());
629 // Initialize the byte array output stream and the conversion buffer byte array.
630 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
631 byte[] conversionBufferByteArray = new byte[1024];
633 // Instantiate the variable to track the buffer length.
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);
642 } catch (IOException e) {
646 // Close the input stream.
649 // Populate the response body string with the contents of the byte array output stream.
650 responseBodyBuilder.append(byteArrayOutputStream.toString());
652 // Disconnect `httpUrlConnection`.
653 httpUrlConnection.disconnect();
655 } catch (IOException e) {
659 // Return the response body string as the result.
660 return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
663 // `onPostExecute()` operates on the UI thread.
665 protected void onPostExecute(SpannableStringBuilder[] viewSourceStringArray){
666 // Get a handle the activity.
667 Activity activity = activityWeakReference.get();
669 // Abort if the activity is gone.
670 if ((activity == null) || (activity.isFinishing())) {
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);
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]);
688 // Hide the progress bar.
689 progressBar.setIndeterminate(false);
690 progressBar.setVisibility(View.GONE);
692 //Stop the swipe to refresh indicator if it is running
693 swipeRefreshLayout.setRefreshing(false);