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 // Reapply the highlighting.
151 // Set the go button on the keyboard to request new source data.
152 urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
153 // Request new source data if the enter key was pressed.
154 if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
155 // Hide the soft keyboard.
156 inputMethodManager.hideSoftInputFromWindow(urlEditText.getWindowToken(), 0);
158 // Remove the focus from the URL box.
159 urlEditText.clearFocus();
161 // Get new source data for the current URL.
162 new GetSource(this).execute(urlEditText.getText().toString());
164 // Consume the key press.
167 // Do not consume the key press.
172 // Implement swipe to refresh.
173 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.view_source_swiperefreshlayout);
174 swipeRefreshLayout.setOnRefreshListener(() -> new GetSource(this).execute(urlEditText.getText().toString()));
176 // Set the swipe to refresh color according to the theme.
177 if (MainWebViewActivity.darkTheme) {
178 swipeRefreshLayout.setColorSchemeResources(R.color.blue_600);
179 swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.gray_800);
181 swipeRefreshLayout.setColorSchemeResources(R.color.blue_700);
184 // Get the source using an AsyncTask.
185 new GetSource(this).execute(formattedUrlString);
189 public boolean onCreateOptionsMenu(Menu menu) {
190 // Inflate the menu; this adds items to the action bar if it is present.
191 getMenuInflater().inflate(R.menu.view_source_options_menu, menu);
198 public boolean onOptionsItemSelected(MenuItem menuItem) {
199 // Get a handle for the about alert dialog.
200 DialogFragment aboutDialogFragment = new AboutViewSourceDialog();
202 // Show the about alert dialog.
203 aboutDialogFragment.show(getFragmentManager(), getString(R.string.about));
205 // Consume the event.
209 public void goBack(View view) {
211 NavUtils.navigateUpFromSameTask(activity);
214 private void highlightUrlText() {
215 // Get a handle for the URL EditText.
216 EditText urlEditText = findViewById(R.id.url_edittext);
218 // Get the URL string.
219 String urlString = urlEditText.getText().toString();
221 // Get the index of the `/` immediately after the domain name.
222 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
224 // Create a base URL string.
228 if (endOfDomainName > 0) { // There is at least one character after the base URL.
230 baseUrl = urlString.substring(0, endOfDomainName);
231 } else { // There are no characters after the base URL.
232 // Set the base URL to be the entire URL string.
236 // Get the index of the last `.` in the domain.
237 int lastDotIndex = baseUrl.lastIndexOf(".");
239 // Get the index of the penultimate `.` in the domain.
240 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
242 // Markup the beginning of the URL.
243 if (urlString.startsWith("http://")) { // Highlight the protocol of connections that are not encrypted.
244 urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
246 // De-emphasize subdomains.
247 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
248 urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
250 } else if (urlString.startsWith("https://")) { // De-emphasize the protocol of connections that are encrypted.
251 if (penultimateDotIndex > 0) { // There is more than one subdomain in the domain name.
252 // De-emphasize the protocol and the additional subdomains.
253 urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
254 } else { // There is only one subdomain in the domain name.
255 // De-emphasize only the protocol.
256 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
260 // De-emphasize the text after the domain name.
261 if (endOfDomainName > 0) {
262 urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
266 // `String` declares the parameters. `Void` does not declare progress units. `String[]` contains the results.
267 private static class GetSource extends AsyncTask<String, Void, SpannableStringBuilder[]> {
268 // Create a weak reference to the calling activity.
269 private WeakReference<Activity> activityWeakReference;
271 // Populate the weak reference to the calling activity.
272 GetSource(Activity activity) {
273 activityWeakReference = new WeakReference<>(activity);
276 // `onPreExecute()` operates on the UI thread.
278 protected void onPreExecute() {
279 // Get a handle for the activity.
280 Activity viewSourceActivity = activityWeakReference.get();
282 // Abort if the activity is gone.
283 if ((viewSourceActivity == null) || (viewSourceActivity.isFinishing())) {
287 // Get a handle for the progress bar.
288 ProgressBar progressBar = viewSourceActivity.findViewById(R.id.progress_bar);
290 // Make the progress bar visible.
291 progressBar.setVisibility(View.VISIBLE);
293 // Set the progress bar to be indeterminate.
294 progressBar.setIndeterminate(true);
298 protected SpannableStringBuilder[] doInBackground(String... formattedUrlString) {
299 // Initialize the response body String.
300 SpannableStringBuilder requestHeadersBuilder = new SpannableStringBuilder();
301 SpannableStringBuilder responseMessageBuilder = new SpannableStringBuilder();
302 SpannableStringBuilder responseHeadersBuilder = new SpannableStringBuilder();
303 SpannableStringBuilder responseBodyBuilder = new SpannableStringBuilder();
305 // Get a handle for the activity.
306 Activity activity = activityWeakReference.get();
308 // Abort if the activity is gone.
309 if ((activity == null) || (activity.isFinishing())) {
310 return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
313 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
315 // Get the current URL from the main activity.
316 URL url = new URL(formattedUrlString[0]);
318 // Open a connection to the URL. No data is actually sent at this point.
319 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
321 // Instantiate the variables necessary to build the request headers.
322 requestHeadersBuilder = new SpannableStringBuilder();
323 int oldRequestHeadersBuilderLength;
324 int newRequestHeadersBuilderLength;
327 // Set the `Host` header property.
328 httpUrlConnection.setRequestProperty("Host", url.getHost());
330 // Add the `Host` header to the string builder and format the text.
331 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
332 requestHeadersBuilder.append("Host", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
333 } else { // Older versions not so much.
334 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
335 requestHeadersBuilder.append("Host");
336 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
337 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
339 requestHeadersBuilder.append(": ");
340 requestHeadersBuilder.append(url.getHost());
343 // Set the `Connection` header property.
344 httpUrlConnection.setRequestProperty("Connection", "keep-alive");
346 // Add the `Connection` header to the string builder and format the text.
347 requestHeadersBuilder.append(System.getProperty("line.separator"));
348 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
349 requestHeadersBuilder.append("Connection", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
350 } else { // Older versions not so much.
351 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
352 requestHeadersBuilder.append("Connection");
353 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
354 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
356 requestHeadersBuilder.append(": keep-alive");
359 // Get the current `User-Agent` string.
360 String userAgentString = MainWebViewActivity.appliedUserAgentString;
362 // Set the `User-Agent` header property.
363 httpUrlConnection.setRequestProperty("User-Agent", userAgentString);
365 // Add the `User-Agent` header to the string builder and format the text.
366 requestHeadersBuilder.append(System.getProperty("line.separator"));
367 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
368 requestHeadersBuilder.append("User-Agent", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
369 } else { // Older versions not so much.
370 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
371 requestHeadersBuilder.append("User-Agent");
372 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
373 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
375 requestHeadersBuilder.append(": ");
376 requestHeadersBuilder.append(userAgentString);
379 // Set the `Upgrade-Insecure-Requests` header property.
380 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
382 // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
383 requestHeadersBuilder.append(System.getProperty("line.separator"));
384 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
385 requestHeadersBuilder.append("Upgrade-Insecure-Requests", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
386 } else { // Older versions not so much.
387 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
388 requestHeadersBuilder.append("Upgrade-Insecure_Requests");
389 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
390 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
392 requestHeadersBuilder.append(": 1");
395 // Set the `x-requested-with` header property.
396 httpUrlConnection.setRequestProperty("x-requested-with", "");
398 // Add the `x-requested-with` header to the string builder and format the text.
399 requestHeadersBuilder.append(System.getProperty("line.separator"));
400 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
401 requestHeadersBuilder.append("x-requested-with", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
402 } else { // Older versions not so much.
403 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
404 requestHeadersBuilder.append("x-requested-with");
405 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
406 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
408 requestHeadersBuilder.append(": ");
411 // Get a handle for the shared preferences.
412 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
414 // Only populate `Do Not Track` if it is enabled.
415 if (sharedPreferences.getBoolean("do_not_track", false)) {
416 // Set the `dnt` header property.
417 httpUrlConnection.setRequestProperty("dnt", "1");
419 // Add the `dnt` header to the string builder and format the text.
420 requestHeadersBuilder.append(System.getProperty("line.separator"));
421 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
422 requestHeadersBuilder.append("dnt", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
423 } else { // Older versions not so much.
424 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
425 requestHeadersBuilder.append("dnt");
426 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
427 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
429 requestHeadersBuilder.append(": 1");
433 // Set the `Accept` header property.
434 httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
436 // Add the `Accept` header to the string builder and format the text.
437 requestHeadersBuilder.append(System.getProperty("line.separator"));
438 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
439 requestHeadersBuilder.append("Accept", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
440 } else { // Older versions not so much.
441 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
442 requestHeadersBuilder.append("Accept");
443 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
444 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
446 requestHeadersBuilder.append(": ");
447 requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
450 // Instantiate a locale string.
453 // Populate the locale string.
454 if (Build.VERSION.SDK_INT >= 24) { // SDK >= 24 has a list of locales.
455 // Get the list of locales.
456 LocaleList localeList = activity.getResources().getConfiguration().getLocales();
458 // Initialize a string builder to extract the locales from the list.
459 StringBuilder localesStringBuilder = new StringBuilder();
461 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
464 // Populate the string builder with the contents of the locales list.
465 for (int i = 0; i < localeList.size(); i++) {
466 // Append a comma if there is already an item in the string builder.
468 localesStringBuilder.append(",");
471 // Get the indicated locale from the list.
472 localesStringBuilder.append(localeList.get(i));
474 // If not the first locale, append `;q=0.i`, which drops by .1 for each removal from the main locale.
476 localesStringBuilder.append(";q=0.");
477 localesStringBuilder.append(q);
484 // Store the populated string builder in the locale string.
485 localeString = localesStringBuilder.toString();
486 } else { // SDK < 24 only has a primary locale.
487 // Store the locale in the locale string.
488 localeString = Locale.getDefault().toString();
491 // Set the `Accept-Language` header property.
492 httpUrlConnection.setRequestProperty("Accept-Language", localeString);
494 // Add the `Accept-Language` header to the string builder and format the text.
495 requestHeadersBuilder.append(System.getProperty("line.separator"));
496 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
497 requestHeadersBuilder.append("Accept-Language", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
498 } else { // Older versions not so much.
499 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
500 requestHeadersBuilder.append("Accept-Language");
501 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
502 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
504 requestHeadersBuilder.append(": ");
505 requestHeadersBuilder.append(localeString);
508 // Get the cookies for the current domain.
509 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
511 // Only process the cookies if they are not null.
512 if (cookiesString != null) {
513 // Set the `Cookie` header property.
514 httpUrlConnection.setRequestProperty("Cookie", cookiesString);
516 // Add the `Cookie` header to the string builder and format the text.
517 requestHeadersBuilder.append(System.getProperty("line.separator"));
518 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
519 requestHeadersBuilder.append("Cookie", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
520 } else { // Older versions not so much.
521 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
522 requestHeadersBuilder.append("Cookie");
523 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
524 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
526 requestHeadersBuilder.append(": ");
527 requestHeadersBuilder.append(cookiesString);
531 // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default. If the property is manually set, than `HttpUrlConnection` does not process the decoding.
532 // Add the `Accept-Encoding` header to the string builder and format the text.
533 requestHeadersBuilder.append(System.getProperty("line.separator"));
534 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
535 requestHeadersBuilder.append("Accept-Encoding", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
536 } else { // Older versions not so much.
537 oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
538 requestHeadersBuilder.append("Accept-Encoding");
539 newRequestHeadersBuilderLength = requestHeadersBuilder.length();
540 requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength + 1, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
542 requestHeadersBuilder.append(": gzip");
545 // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
547 // Initialize the string builders.
548 responseMessageBuilder = new SpannableStringBuilder();
549 responseHeadersBuilder = new SpannableStringBuilder();
551 // Get the response code, which causes the connection to the server to be made.
552 int responseCode = httpUrlConnection.getResponseCode();
554 // Populate the response message string builder.
555 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
556 responseMessageBuilder.append(String.valueOf(responseCode), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
557 } else { // Older versions not so much.
558 responseMessageBuilder.append(String.valueOf(responseCode));
559 int newLength = responseMessageBuilder.length();
560 responseMessageBuilder.setSpan(new StyleSpan(Typeface.BOLD), 0, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
562 responseMessageBuilder.append(": ");
563 responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
565 // Initialize the iteration variable.
568 // Iterate through the received header fields.
569 while (httpUrlConnection.getHeaderField(i) != null) {
570 // Add a new line if there is already information in the string builder.
572 responseHeadersBuilder.append(System.getProperty("line.separator"));
575 // Add the header to the string builder and format the text.
576 if (Build.VERSION.SDK_INT >= 21) { // Newer versions of Android are so smart.
577 responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
578 } else { // Older versions not so much.
579 int oldLength = responseHeadersBuilder.length();
580 responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i));
581 int newLength = responseHeadersBuilder.length();
582 responseHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldLength + 1, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
584 responseHeadersBuilder.append(": ");
585 responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
587 // Increment the iteration variable.
591 // Instantiate an input stream for the response body.
592 InputStream inputStream;
594 // Get the correct input stream based on the response code.
595 if (responseCode == 404) { // Get the error stream.
596 inputStream = new BufferedInputStream(httpUrlConnection.getErrorStream());
597 } else { // Get the response body stream.
598 inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
601 // Initialize the byte array output stream and the conversion buffer byte array.
602 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
603 byte[] conversionBufferByteArray = new byte[1024];
605 // Instantiate the variable to track the buffer length.
609 // Attempt to read data from the input stream and store it in the conversion buffer byte array. Also store the amount of data transferred in the buffer length variable.
610 while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) { // Proceed while the amount of data stored in the buffer is > 0.
611 // Write the contents of the conversion buffer to the byte array output stream.
612 byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength);
614 } catch (IOException e) {
618 // Close the input stream.
621 // Populate the response body string with the contents of the byte array output stream.
622 responseBodyBuilder.append(byteArrayOutputStream.toString());
624 // Disconnect `httpUrlConnection`.
625 httpUrlConnection.disconnect();
627 } catch (IOException e) {
631 // Return the response body string as the result.
632 return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
635 // `onPostExecute()` operates on the UI thread.
637 protected void onPostExecute(SpannableStringBuilder[] viewSourceStringArray){
638 // Get a handle the activity.
639 Activity activity = activityWeakReference.get();
641 // Abort if the activity is gone.
642 if ((activity == null) || (activity.isFinishing())) {
646 // Get handles for the text views.
647 TextView requestHeadersTextView = activity.findViewById(R.id.request_headers);
648 TextView responseMessageTextView = activity.findViewById(R.id.response_message);
649 TextView responseHeadersTextView = activity.findViewById(R.id.response_headers);
650 TextView responseBodyTextView = activity.findViewById(R.id.response_body);
651 ProgressBar progressBar = activity.findViewById(R.id.progress_bar);
652 SwipeRefreshLayout swipeRefreshLayout = activity.findViewById(R.id.view_source_swiperefreshlayout);
654 // Populate the text views. This can take a long time, and freeze the user interface, if the response body is particularly large.
655 requestHeadersTextView.setText(viewSourceStringArray[0]);
656 responseMessageTextView.setText(viewSourceStringArray[1]);
657 responseHeadersTextView.setText(viewSourceStringArray[2]);
658 responseBodyTextView.setText(viewSourceStringArray[3]);
660 // Hide the progress bar.
661 progressBar.setIndeterminate(false);
662 progressBar.setVisibility(View.GONE);
664 //Stop the swipe to refresh indicator if it is running
665 swipeRefreshLayout.setRefreshing(false);