]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/commitdiff
Use the secret built-in View Source. https://redmine.stoutner.com/issues/1023
authorSoren Stoutner <soren@stoutner.com>
Wed, 21 Jun 2023 21:43:17 +0000 (14:43 -0700)
committerSoren Stoutner <soren@stoutner.com>
Wed, 21 Jun 2023 21:43:17 +0000 (14:43 -0700)
30 files changed:
app/src/main/AndroidManifest.xml
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt
app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt [deleted file]
app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetHeadersBackgroundTask.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt [deleted file]
app/src/main/java/com/stoutner/privacybrowser/dialogs/AboutViewHeadersDialog.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/dialogs/AboutViewSourceDialog.kt [deleted file]
app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/ViewHeadersFactory.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/WebViewSourceFactory.kt [deleted file]
app/src/main/java/com/stoutner/privacybrowser/viewmodels/HeadersViewModel.kt [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/viewmodels/WebViewSource.kt [deleted file]
app/src/main/res/layout/view_headers_appbar_custom_view.xml [new file with mode: 0644]
app/src/main/res/layout/view_headers_bottom_appbar.xml [new file with mode: 0644]
app/src/main/res/layout/view_headers_top_appbar.xml [new file with mode: 0644]
app/src/main/res/layout/view_source_appbar_custom_view.xml [deleted file]
app/src/main/res/layout/view_source_bottom_appbar.xml [deleted file]
app/src/main/res/layout/view_source_top_appbar.xml [deleted file]
app/src/main/res/menu/view_headers_options_menu.xml [new file with mode: 0644]
app/src/main/res/menu/view_source_options_menu.xml [deleted file]
app/src/main/res/menu/webview_options_menu.xml
app/src/main/res/values-de/strings.xml
app/src/main/res/values-es/strings.xml
app/src/main/res/values-fr/strings.xml
app/src/main/res/values-it/strings.xml
app/src/main/res/values-pt-rBR/strings.xml
app/src/main/res/values-ru/strings.xml
app/src/main/res/values-tr/strings.xml
app/src/main/res/values-zh-rCN/strings.xml
app/src/main/res/values/strings.xml

index 4cfa09bd9aa54ee328ed82048781cdbf24caba59..968bfeb11297f97560cd6d3c3c3e685a87573a09 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright 2015-2022 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
 
             `android:configChanges="keyboard|keyboardHidden"` makes the activity not restart when a bluetooth keyboard is activated/goes to sleep.
             `android:persistableMode="persistNever"` removes Privacy Browser from the recent apps list on a device reboot. -->
         <activity
-            android:name=".activities.ViewSourceActivity"
-            android:label="@string/view_source"
+            android:name=".activities.ViewHeadersActivity"
+            android:label="@string/view_headers"
             android:parentActivityName=".activities.MainWebViewActivity"
             android:configChanges="orientation|screenSize|screenLayout|keyboard|keyboardHidden"
             android:screenOrientation="fullUser"
index 8fb1bc38900941cfceedfbcd400f160bb12f6138..c6d91350ea3a2116eca251efa663e3bbd276749f 100644 (file)
@@ -321,6 +321,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
     private lateinit var optionsUserAgentSafariOnIosMenuItem: MenuItem
     private lateinit var optionsUserAgentSafariOnMacosMenuItem: MenuItem
     private lateinit var optionsUserAgentWebViewDefaultMenuItem: MenuItem
+    private lateinit var optionsViewSourceMenuItem: MenuItem
     private lateinit var optionsWideViewportMenuItem: MenuItem
     private lateinit var proxyHelper: ProxyHelper
     private lateinit var redColorSpan: ForegroundColorSpan
@@ -685,6 +686,9 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
                         // Go back.
                         currentWebView!!.goBack()
+
+                        // Update the URL edit text after a delay.
+                        updateUrlEditTextAfterDelay()
                     } else {  // Close the current tab.
                         // A view is required because the method is also called by an XML `onClick`.
                         closeTab(null)
@@ -1063,6 +1067,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         optionsDisplayImagesMenuItem = menu.findItem(R.id.display_images)
         optionsDarkWebViewMenuItem = menu.findItem(R.id.dark_webview)
         optionsFontSizeMenuItem = menu.findItem(R.id.font_size)
+        optionsViewSourceMenuItem = menu.findItem(R.id.view_source)
         optionsAddOrEditDomainMenuItem = menu.findItem(R.id.add_or_edit_domain)
 
         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
@@ -1137,7 +1142,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             optionsWideViewportMenuItem.isChecked = currentWebView!!.settings.useWideViewPort
             optionsDisplayImagesMenuItem.isChecked = currentWebView!!.settings.loadsImagesAutomatically
 
-            // Initialize the display names for the filter lists with the number of blocked requests.
+            // Set the display names for the filter lists with the number of blocked requests.
             optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + currentWebView!!.getRequestsCount(BLOCKED_REQUESTS)
             optionsEasyListMenuItem.title = currentWebView!!.getRequestsCount(EASYLIST).toString() + " - " + getString(R.string.easylist)
             optionsEasyPrivacyMenuItem.title = currentWebView!!.getRequestsCount(EASYPRIVACY).toString() + " - " + getString(R.string.easyprivacy)
@@ -1159,6 +1164,12 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             // Set the checkbox status for dark WebView if algorithmic darkening is supported.
             if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING))
                 optionsDarkWebViewMenuItem.isChecked = WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView!!.settings)
+
+            // Set the view source title according to the current URL.
+            if (currentWebView!!.currentUrl.startsWith("view-source:"))
+                optionsViewSourceMenuItem.title = getString(R.string.view_rendered_website)
+            else
+                optionsViewSourceMenuItem.title = getString(R.string.view_source)
         }
 
         // Set the cookies menu item checked status.
@@ -2003,15 +2014,29 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             }
 
             R.id.view_source -> {  // View source.
-                // Create an intent to launch the view source activity.
-                val viewSourceIntent = Intent(this, ViewSourceActivity::class.java)
+                // Open a new tab according to the current URL.
+                if (currentWebView!!.currentUrl.startsWith("view-source:")) {  // The source is currently viewed.
+                    // Open the rendered website in a new tab.
+                    addNewTab(currentWebView!!.currentUrl.substring(12, currentWebView!!.currentUrl.length), true)
+                } else {  // The rendered website is currently viewed.
+                    // Open the source in a new tab.
+                    addNewTab("view-source:${currentWebView!!.currentUrl}", true)
+                }
+
+                // Consume the event.
+                true
+            }
+
+            R.id.view_headers -> {  // View headers.
+                // Create an intent to launch the view headers activity.
+                val viewHeadersIntent = Intent(this, ViewHeadersActivity::class.java)
 
                 // Add the variables to the intent.
-                viewSourceIntent.putExtra(CURRENT_URL, currentWebView!!.url)
-                viewSourceIntent.putExtra(USER_AGENT, currentWebView!!.settings.userAgentString)
+                viewHeadersIntent.putExtra(CURRENT_URL, currentWebView!!.url)
+                viewHeadersIntent.putExtra(USER_AGENT, currentWebView!!.settings.userAgentString)
 
                 // Make it so.
-                startActivity(viewSourceIntent)
+                startActivity(viewHeadersIntent)
 
                 // Consume the event.
                 true
@@ -2209,6 +2234,9 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
                     // Load the previous website in the history.
                     currentWebView!!.goBack()
+
+                    // Update the URL edit text after a delay.
+                    updateUrlEditTextAfterDelay()
                 }
             }
 
@@ -2226,6 +2254,9 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
                     // Load the next website in the history.
                     currentWebView!!.goForward()
+
+                    // Update the URL edit text after a delay.
+                    updateUrlEditTextAfterDelay()
                 }
             }
 
@@ -3394,6 +3425,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         if (reloadWebsite)
             nestedScrollWebView.reload()
 
+        // Disable the wide viewport if the source is being viewed.
+        if (url.startsWith("view-source:"))
+            nestedScrollWebView.settings.useWideViewPort = false
+
         // Load the URL if directed.  This makes sure that the domain settings are properly loaded before the URL.  By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
         if (loadUrl)
             nestedScrollWebView.loadUrl(url)
@@ -5346,8 +5381,8 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
                 // Update the URL text bar if the page is currently selected and the URL edit text is not currently being edited.
                 if ((tabLayout.selectedTabPosition == currentPagePosition) && !urlEditText.hasFocus()) {
-                    // Display the formatted URL text.
-                    urlEditText.setText(url)
+                    // Display the formatted URL text.  The nested scroll WebView current URL preserves any initial `view-source:`, and opposed to the method URL variable.
+                    urlEditText.setText(nestedScrollWebView.currentUrl)
 
                     // Highlight the URL syntax.
                     UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
@@ -5689,7 +5724,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         var urlString = ""
 
         // Check to see if the unformatted URL string is a valid URL.  Otherwise, convert it into a search.
-        if (unformattedUrlString.startsWith("content://")) {  // This is a content URL.
+        if (unformattedUrlString.startsWith("content://") || unformattedUrlString.startsWith("view-source:")) {  // This is a content or source URL.
             // Load the entire content URL.
             urlString = unformattedUrlString
         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
@@ -5753,6 +5788,9 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
         // Load the history entry.
         currentWebView!!.goBackOrForward(steps)
+
+        // Update the URL edit text after a delay.
+        updateUrlEditTextAfterDelay()
     }
 
     override fun openFile(dialogFragment: DialogFragment) {
@@ -5863,6 +5901,9 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
 
         // Go back.
         currentWebView!!.goBack()
+
+        // Update the URL edit text after a delay.
+        updateUrlEditTextAfterDelay()
     }
 
     private fun sanitizeUrl(urlString: String): String {
@@ -6082,4 +6123,22 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 invalidateOptionsMenu()
         }
     }
+
+    fun updateUrlEditTextAfterDelay() {
+        // Create a handler to update the URL edit box.
+        val urlEditTextUpdateHandler = Handler(Looper.getMainLooper())
+
+        // Create a runnable to update the URL edit box.
+        val urlEditTextUpdateRunnable = Runnable {
+            // Update the URL edit text.
+            urlEditText.setText(currentWebView!!.url)
+
+            // Disable the wide viewport if the source is being viewed.
+            if (currentWebView!!.url!!.startsWith("view-source:"))
+                currentWebView!!.settings.useWideViewPort = false
+        }
+
+        // Update the URL edit text after 50 milliseconds, so that the WebView has enough time to navigate to the new URL.
+        urlEditTextUpdateHandler.postDelayed(urlEditTextUpdateRunnable, 50)
+    }
 }
diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt
new file mode 100644 (file)
index 0000000..2f0aeb0
--- /dev/null
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.activities
+
+import android.os.Bundle
+import android.text.SpannableStringBuilder
+import android.text.style.ForegroundColorSpan
+import android.util.TypedValue
+import android.view.KeyEvent
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.View.OnFocusChangeListener
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import android.widget.ProgressBar
+import android.widget.TextView
+
+import androidx.appcompat.app.ActionBar
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.core.app.NavUtils
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.preference.PreferenceManager
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+
+import com.google.android.material.snackbar.Snackbar
+
+import com.stoutner.privacybrowser.R
+import com.stoutner.privacybrowser.dialogs.AboutViewHeadersDialog
+import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog
+import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog.UntrustedSslCertificateListener
+import com.stoutner.privacybrowser.helpers.ProxyHelper
+import com.stoutner.privacybrowser.helpers.UrlHelper
+import com.stoutner.privacybrowser.viewmodelfactories.ViewHeadersFactory
+import com.stoutner.privacybrowser.viewmodels.HeadersViewModel
+
+// Define the public constants.
+const val USER_AGENT = "user_agent"
+
+class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener {
+    // Declare the class variables.
+    private lateinit var headersViewModel: HeadersViewModel
+    private lateinit var initialGrayColorSpan: ForegroundColorSpan
+    private lateinit var finalGrayColorSpan: ForegroundColorSpan
+    private lateinit var redColorSpan: ForegroundColorSpan
+
+    // Declare the class views.
+    private lateinit var urlEditText: EditText
+    private lateinit var requestHeadersTitleTextView: TextView
+    private lateinit var requestHeadersTextView: TextView
+    private lateinit var responseMessageTitleTextView: TextView
+    private lateinit var responseMessageTextView: TextView
+    private lateinit var responseHeadersTitleTextView: TextView
+    private lateinit var responseBodyTitleTextView: TextView
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        // Get a handle for the shared preferences.
+        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+
+        // Get the preferences.
+        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+        val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
+
+        // Disable screenshots if not allowed.
+        if (!allowScreenshots) {
+            window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        }
+
+        // Run the default commands.
+        super.onCreate(savedInstanceState)
+
+        // Get the launching intent
+        val intent = intent
+
+        // Get the information from the intent.
+        val currentUrl = intent.getStringExtra(CURRENT_URL)!!
+        val userAgent = intent.getStringExtra(USER_AGENT)!!
+
+        // Set the content view.
+        if (bottomAppBar) {
+            setContentView(R.layout.view_headers_bottom_appbar)
+        } else {
+            setContentView(R.layout.view_headers_top_appbar)
+        }
+
+        // Get a handle for the toolbar.
+        val toolbar = findViewById<Toolbar>(R.id.toolbar)
+
+        // Set the support action bar.
+        setSupportActionBar(toolbar)
+
+        // Get a handle for the action bar.
+        val actionBar = supportActionBar!!
+
+        // Add the custom layout to the action bar.
+        actionBar.setCustomView(R.layout.view_headers_appbar_custom_view)
+
+        // Instruct the action bar to display a custom layout.
+        actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
+
+        // Get handles for the views.
+        urlEditText = findViewById(R.id.url_edittext)
+        val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
+        val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.swiperefreshlayout)
+        requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
+        requestHeadersTextView = findViewById(R.id.request_headers_textview)
+        responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
+        responseMessageTextView = findViewById(R.id.response_message_textview)
+        responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textview)
+        val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
+        responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
+        val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
+
+        // Populate the URL text box.
+        urlEditText.setText(currentUrl)
+
+        // Initialize the gray foreground color spans for highlighting the URLs.
+        initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
+        finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
+        redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
+
+        // Apply text highlighting to the URL.
+        UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
+
+        // Get a handle for the input method manager, which is used to hide the keyboard.
+        val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
+
+        // Remove the formatting from the URL when the user is editing the text.
+        urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
+            if (hasFocus) {  // The user is editing the URL text box.
+                // Remove the highlighting.
+                urlEditText.text.removeSpan(redColorSpan)
+                urlEditText.text.removeSpan(initialGrayColorSpan)
+                urlEditText.text.removeSpan(finalGrayColorSpan)
+            } else {  // The user has stopped editing the URL text box.
+                // Hide the soft keyboard.
+                inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
+
+                // Move to the beginning of the string.
+                urlEditText.setSelection(0)
+
+                // Reapply the highlighting.
+                UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
+            }
+        }
+
+        // Set the refresh color scheme according to the theme.
+        swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
+
+        // Initialize a color background typed value.
+        val colorBackgroundTypedValue = TypedValue()
+
+        // Get the color background from the theme.
+        theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
+
+        // Get the color background int from the typed value.
+        val colorBackgroundInt = colorBackgroundTypedValue.data
+
+        // Set the swipe refresh background color.
+        swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
+
+        // Get the list of locales.
+        val localeList = resources.configuration.locales
+
+        // Initialize a string builder to extract the locales from the list.
+        val localesStringBuilder = StringBuilder()
+
+        // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
+        var q = 10
+
+        // Populate the string builder with the contents of the locales list.
+        for (i in 0 until localeList.size()) {
+            // Append a comma if there is already an item in the string builder.
+            if (i > 0) {
+                localesStringBuilder.append(",")
+            }
+
+            // Get the locale from the list.
+            val locale = localeList[i]
+
+            // Add the locale to the string.  `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
+            localesStringBuilder.append(locale.language)
+            localesStringBuilder.append("-")
+            localesStringBuilder.append(locale.country)
+
+            // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
+            if (q < 10) {
+                localesStringBuilder.append(";q=0.")
+                localesStringBuilder.append(q)
+            }
+
+            // Decrement `q` if it is greater than 1.
+            if (q > 1) {
+                q--
+            }
+
+            // Add a second entry for the language only portion of the locale.
+            localesStringBuilder.append(",")
+            localesStringBuilder.append(locale.language)
+
+            // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
+            localesStringBuilder.append(";q=0.")
+            localesStringBuilder.append(q)
+
+            // Decrement `q` if it is greater than 1.
+            if (q > 1) {
+                q--
+            }
+        }
+
+        // Instantiate the proxy helper.
+        val proxyHelper = ProxyHelper()
+
+        // Get the current proxy.
+        val proxy = proxyHelper.getCurrentProxy(this)
+
+        // Make the progress bar visible.
+        progressBar.visibility = View.VISIBLE
+
+        // Set the progress bar to be indeterminate.
+        progressBar.isIndeterminate = true
+
+        // Update the layout.
+        updateLayout(currentUrl)
+
+        // Instantiate the view headers factory.
+        val viewHeadersFactory: ViewModelProvider.Factory = ViewHeadersFactory(currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
+
+        // Instantiate the headers view model.
+        headersViewModel = ViewModelProvider(this, viewHeadersFactory)[HeadersViewModel::class.java]
+
+        // Create a headers observer.
+        headersViewModel.observeHeaders().observe(this) { headersStringArray: Array<SpannableStringBuilder> ->
+            // Populate the text views.  This can take a long time, and freezes the user interface, if the response body is particularly large.
+            requestHeadersTextView.text = headersStringArray[0]
+            responseMessageTextView.text = headersStringArray[1]
+            responseHeadersTextView.text = headersStringArray[2]
+            responseBodyTextView.text = headersStringArray[3]
+
+            // Hide the progress bar.
+            progressBar.isIndeterminate = false
+            progressBar.visibility = View.GONE
+
+            //Stop the swipe to refresh indicator if it is running
+            swipeRefreshLayout.isRefreshing = false
+        }
+
+        // Create an error observer.
+        headersViewModel.observeErrors().observe(this) { errorString: String ->
+            // Display an error snackbar if the string is not `""`.
+            if (errorString != "") {
+                if (errorString.startsWith("javax.net.ssl.SSLHandshakeException")) {
+                    // Instantiate the untrusted SSL certificate dialog.
+                    val untrustedSslCertificateDialog = UntrustedSslCertificateDialog()
+
+                    // Show the untrusted SSL certificate dialog.
+                    untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
+                } else {
+                    // Display a snackbar with the error message.
+                    Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
+                }
+            }
+        }
+
+        // Implement swipe to refresh.
+        swipeRefreshLayout.setOnRefreshListener {
+            // Make the progress bar visible.
+            progressBar.visibility = View.VISIBLE
+
+            // Set the progress bar to be indeterminate.
+            progressBar.isIndeterminate = true
+
+            // Get the URL.
+            val urlString = urlEditText.text.toString()
+
+            // Update the layout.
+            updateLayout(urlString)
+
+            // Get the updated headers.
+            headersViewModel.updateHeaders(urlString, false)
+        }
+
+        // Set the go button on the keyboard to request new headers data.
+        urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
+            // Request new headers data if the enter key was pressed.
+            if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
+                // Hide the soft keyboard.
+                inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
+
+                // Remove the focus from the URL box.
+                urlEditText.clearFocus()
+
+                // Make the progress bar visible.
+                progressBar.visibility = View.VISIBLE
+
+                // Set the progress bar to be indeterminate.
+                progressBar.isIndeterminate = true
+
+                // Get the URL.
+                val urlString = urlEditText.text.toString()
+
+                // Update the layout.
+                updateLayout(urlString)
+
+                // Get the updated headers.
+                headersViewModel.updateHeaders(urlString, false)
+
+                // Consume the key press.
+                return@setOnKeyListener true
+            } else {
+                // Do not consume the key press.
+                return@setOnKeyListener false
+            }
+        }
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        // Inflate the menu.
+        menuInflater.inflate(R.menu.view_headers_options_menu, menu)
+
+        // Display the menu.
+        return true
+    }
+
+    override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
+        // Instantiate the about dialog fragment.
+        val aboutDialogFragment: DialogFragment = AboutViewHeadersDialog()
+
+        // Show the about alert dialog.
+        aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
+
+        // Consume the event.
+        return true
+    }
+
+    // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
+    fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
+        // Go home.
+        NavUtils.navigateUpFromSameTask(this)
+    }
+
+    override fun loadAnyway() {
+        // Load the URL anyway.
+        headersViewModel.updateHeaders(urlEditText.text.toString(), true)
+    }
+
+    private fun updateLayout(urlString: String) {
+        if (urlString.startsWith("content://")) {  // This is a content URL.
+            // Hide the unused text views.
+            requestHeadersTitleTextView.visibility = View.GONE
+            requestHeadersTextView.visibility = View.GONE
+            responseMessageTitleTextView.visibility = View.GONE
+            responseMessageTextView.visibility = View.GONE
+
+            // Change the text of the remaining title text views.
+            responseHeadersTitleTextView.setText(R.string.content_metadata)
+            responseBodyTitleTextView.setText(R.string.content_data)
+        } else {  // This is not a content URL.
+            // Show the views.
+            requestHeadersTitleTextView.visibility = View.VISIBLE
+            requestHeadersTextView.visibility = View.VISIBLE
+            responseMessageTitleTextView.visibility = View.VISIBLE
+            responseMessageTextView.visibility = View.VISIBLE
+
+            // Restore the text of the other title text views.
+            responseHeadersTitleTextView.setText(R.string.response_headers)
+            responseBodyTitleTextView.setText(R.string.response_body)
+        }
+    }
+}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt
deleted file mode 100644 (file)
index 736f67d..0000000
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.activities
-
-import android.os.Bundle
-import android.text.SpannableStringBuilder
-import android.text.style.ForegroundColorSpan
-import android.util.TypedValue
-import android.view.KeyEvent
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.view.View.OnFocusChangeListener
-import android.view.WindowManager
-import android.view.inputmethod.InputMethodManager
-import android.widget.EditText
-import android.widget.ProgressBar
-import android.widget.TextView
-
-import androidx.appcompat.app.ActionBar
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.Toolbar
-import androidx.core.app.NavUtils
-import androidx.fragment.app.DialogFragment
-import androidx.lifecycle.ViewModelProvider
-import androidx.preference.PreferenceManager
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-
-import com.google.android.material.snackbar.Snackbar
-
-import com.stoutner.privacybrowser.R
-import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog
-import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog
-import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog.UntrustedSslCertificateListener
-import com.stoutner.privacybrowser.helpers.ProxyHelper
-import com.stoutner.privacybrowser.helpers.UrlHelper
-import com.stoutner.privacybrowser.viewmodelfactories.WebViewSourceFactory
-import com.stoutner.privacybrowser.viewmodels.WebViewSource
-
-// Define the public constants.
-const val USER_AGENT = "user_agent"
-
-class ViewSourceActivity: AppCompatActivity(), UntrustedSslCertificateListener {
-    // Declare the class variables.
-    private lateinit var initialGrayColorSpan: ForegroundColorSpan
-    private lateinit var finalGrayColorSpan: ForegroundColorSpan
-    private lateinit var redColorSpan: ForegroundColorSpan
-    private lateinit var webViewSource: WebViewSource
-
-    // Declare the class views.
-    private lateinit var urlEditText: EditText
-    private lateinit var requestHeadersTitleTextView: TextView
-    private lateinit var requestHeadersTextView: TextView
-    private lateinit var responseMessageTitleTextView: TextView
-    private lateinit var responseMessageTextView: TextView
-    private lateinit var responseHeadersTitleTextView: TextView
-    private lateinit var responseBodyTitleTextView: TextView
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        // Get a handle for the shared preferences.
-        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
-
-        // Get the preferences.
-        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
-        val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
-
-        // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
-            window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
-        }
-
-        // Run the default commands.
-        super.onCreate(savedInstanceState)
-
-        // Get the launching intent
-        val intent = intent
-
-        // Get the information from the intent.
-        val currentUrl = intent.getStringExtra(CURRENT_URL)!!
-        val userAgent = intent.getStringExtra(USER_AGENT)!!
-
-        // Set the content view.
-        if (bottomAppBar) {
-            setContentView(R.layout.view_source_bottom_appbar)
-        } else {
-            setContentView(R.layout.view_source_top_appbar)
-        }
-
-        // Get a handle for the toolbar.
-        val toolbar = findViewById<Toolbar>(R.id.view_source_toolbar)
-
-        // Set the support action bar.
-        setSupportActionBar(toolbar)
-
-        // Get a handle for the action bar.
-        val actionBar = supportActionBar!!
-
-        // Add the custom layout to the action bar.
-        actionBar.setCustomView(R.layout.view_source_appbar_custom_view)
-
-        // Instruct the action bar to display a custom layout.
-        actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
-
-        // Get handles for the views.
-        urlEditText = findViewById(R.id.url_edittext)
-        val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
-        val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.view_source_swiperefreshlayout)
-        requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
-        requestHeadersTextView = findViewById(R.id.request_headers_textview)
-        responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
-        responseMessageTextView = findViewById(R.id.response_message_textview)
-        responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textview)
-        val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
-        responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
-        val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
-
-        // Populate the URL text box.
-        urlEditText.setText(currentUrl)
-
-        // Initialize the gray foreground color spans for highlighting the URLs.
-        initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
-        finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
-        redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
-
-        // Apply text highlighting to the URL.
-        UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
-
-        // Get a handle for the input method manager, which is used to hide the keyboard.
-        val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
-
-        // Remove the formatting from the URL when the user is editing the text.
-        urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
-            if (hasFocus) {  // The user is editing the URL text box.
-                // Remove the highlighting.
-                urlEditText.text.removeSpan(redColorSpan)
-                urlEditText.text.removeSpan(initialGrayColorSpan)
-                urlEditText.text.removeSpan(finalGrayColorSpan)
-            } else {  // The user has stopped editing the URL text box.
-                // Hide the soft keyboard.
-                inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
-
-                // Move to the beginning of the string.
-                urlEditText.setSelection(0)
-
-                // Reapply the highlighting.
-                UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
-            }
-        }
-
-        // Set the refresh color scheme according to the theme.
-        swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
-
-        // Initialize a color background typed value.
-        val colorBackgroundTypedValue = TypedValue()
-
-        // Get the color background from the theme.
-        theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
-
-        // Get the color background int from the typed value.
-        val colorBackgroundInt = colorBackgroundTypedValue.data
-
-        // Set the swipe refresh background color.
-        swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
-
-        // Get the list of locales.
-        val localeList = resources.configuration.locales
-
-        // Initialize a string builder to extract the locales from the list.
-        val localesStringBuilder = StringBuilder()
-
-        // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
-        var q = 10
-
-        // Populate the string builder with the contents of the locales list.
-        for (i in 0 until localeList.size()) {
-            // Append a comma if there is already an item in the string builder.
-            if (i > 0) {
-                localesStringBuilder.append(",")
-            }
-
-            // Get the locale from the list.
-            val locale = localeList[i]
-
-            // Add the locale to the string.  `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
-            localesStringBuilder.append(locale.language)
-            localesStringBuilder.append("-")
-            localesStringBuilder.append(locale.country)
-
-            // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
-            if (q < 10) {
-                localesStringBuilder.append(";q=0.")
-                localesStringBuilder.append(q)
-            }
-
-            // Decrement `q` if it is greater than 1.
-            if (q > 1) {
-                q--
-            }
-
-            // Add a second entry for the language only portion of the locale.
-            localesStringBuilder.append(",")
-            localesStringBuilder.append(locale.language)
-
-            // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
-            localesStringBuilder.append(";q=0.")
-            localesStringBuilder.append(q)
-
-            // Decrement `q` if it is greater than 1.
-            if (q > 1) {
-                q--
-            }
-        }
-
-        // Instantiate the proxy helper.
-        val proxyHelper = ProxyHelper()
-
-        // Get the current proxy.
-        val proxy = proxyHelper.getCurrentProxy(this)
-
-        // Make the progress bar visible.
-        progressBar.visibility = View.VISIBLE
-
-        // Set the progress bar to be indeterminate.
-        progressBar.isIndeterminate = true
-
-        // Update the layout.
-        updateLayout(currentUrl)
-
-        // Instantiate the WebView source factory.
-        val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
-
-        // Instantiate the WebView source view model class.
-        webViewSource = ViewModelProvider(this, webViewSourceFactory)[WebViewSource::class.java]
-
-        // Create a source observer.
-        webViewSource.observeSource().observe(this) { sourceStringArray: Array<SpannableStringBuilder> ->
-            // Populate the text views.  This can take a long time, and freezes the user interface, if the response body is particularly large.
-            requestHeadersTextView.text = sourceStringArray[0]
-            responseMessageTextView.text = sourceStringArray[1]
-            responseHeadersTextView.text = sourceStringArray[2]
-            responseBodyTextView.text = sourceStringArray[3]
-
-            // Hide the progress bar.
-            progressBar.isIndeterminate = false
-            progressBar.visibility = View.GONE
-
-            //Stop the swipe to refresh indicator if it is running
-            swipeRefreshLayout.isRefreshing = false
-        }
-
-        // Create an error observer.
-        webViewSource.observeErrors().observe(this) { errorString: String ->
-            // Display an error snackbar if the string is not `""`.
-            if (errorString != "") {
-                if (errorString.startsWith("javax.net.ssl.SSLHandshakeException")) {
-                    // Instantiate the untrusted SSL certificate dialog.
-                    val untrustedSslCertificateDialog = UntrustedSslCertificateDialog()
-
-                    // Show the untrusted SSL certificate dialog.
-                    untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
-                } else {
-                    // Display a snackbar with the error message.
-                    Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
-                }
-            }
-        }
-
-        // Implement swipe to refresh.
-        swipeRefreshLayout.setOnRefreshListener {
-            // Make the progress bar visible.
-            progressBar.visibility = View.VISIBLE
-
-            // Set the progress bar to be indeterminate.
-            progressBar.isIndeterminate = true
-
-            // Get the URL.
-            val urlString = urlEditText.text.toString()
-
-            // Update the layout.
-            updateLayout(urlString)
-
-            // Get the updated source.
-            webViewSource.updateSource(urlString, false)
-        }
-
-        // Set the go button on the keyboard to request new source data.
-        urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
-            // Request new source data if the enter key was pressed.
-            if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
-                // Hide the soft keyboard.
-                inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
-
-                // Remove the focus from the URL box.
-                urlEditText.clearFocus()
-
-                // Make the progress bar visible.
-                progressBar.visibility = View.VISIBLE
-
-                // Set the progress bar to be indeterminate.
-                progressBar.isIndeterminate = true
-
-                // Get the URL.
-                val urlString = urlEditText.text.toString()
-
-                // Update the layout.
-                updateLayout(urlString)
-
-                // Get the updated source.
-                webViewSource.updateSource(urlString, false)
-
-                // Consume the key press.
-                return@setOnKeyListener true
-            } else {
-                // Do not consume the key press.
-                return@setOnKeyListener false
-            }
-        }
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu): Boolean {
-        // Inflate the menu.
-        menuInflater.inflate(R.menu.view_source_options_menu, menu)
-
-        // Display the menu.
-        return true
-    }
-
-    override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
-        // Instantiate the about dialog fragment.
-        val aboutDialogFragment: DialogFragment = AboutViewSourceDialog()
-
-        // Show the about alert dialog.
-        aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
-
-        // Consume the event.
-        return true
-    }
-
-    // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
-    fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
-        // Go home.
-        NavUtils.navigateUpFromSameTask(this)
-    }
-
-    override fun loadAnyway() {
-        // Load the URL anyway.
-        webViewSource.updateSource(urlEditText.text.toString(), true)
-    }
-
-    private fun updateLayout(urlString: String) {
-        if (urlString.startsWith("content://")) {  // This is a content URL.
-            // Hide the unused text views.
-            requestHeadersTitleTextView.visibility = View.GONE
-            requestHeadersTextView.visibility = View.GONE
-            responseMessageTitleTextView.visibility = View.GONE
-            responseMessageTextView.visibility = View.GONE
-
-            // Change the text of the remaining title text views.
-            responseHeadersTitleTextView.setText(R.string.content_metadata)
-            responseBodyTitleTextView.setText(R.string.content_data)
-        } else {  // This is not a content URL.
-            // Show the views.
-            requestHeadersTitleTextView.visibility = View.VISIBLE
-            requestHeadersTextView.visibility = View.VISIBLE
-            responseMessageTitleTextView.visibility = View.VISIBLE
-            responseMessageTextView.visibility = View.VISIBLE
-
-            // Restore the text of the other title text views.
-            responseHeadersTitleTextView.setText(R.string.response_headers)
-            responseBodyTitleTextView.setText(R.string.response_body)
-        }
-    }
-}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetHeadersBackgroundTask.kt b/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetHeadersBackgroundTask.kt
new file mode 100644 (file)
index 0000000..030ce4c
--- /dev/null
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.backgroundtasks
+
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.graphics.Typeface
+import android.net.Uri
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.StyleSpan
+import android.webkit.CookieManager
+
+import com.stoutner.privacybrowser.viewmodels.HeadersViewModel
+
+import java.io.BufferedInputStream
+import java.io.BufferedReader
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.InputStreamReader
+
+import java.net.HttpURLConnection
+import java.net.Proxy
+import java.net.URL
+
+import java.security.SecureRandom
+import java.security.cert.X509Certificate
+
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.HttpsURLConnection
+import javax.net.ssl.SSLContext
+import javax.net.ssl.SSLSession
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
+
+
+class GetHeadersBackgroundTask {
+    fun acquire(urlString: String, userAgent: String, localeString: String, proxy: Proxy, contentResolver: ContentResolver, headersViewModel: HeadersViewModel, ignoreSslErrors: Boolean):
+            Array<SpannableStringBuilder> {
+
+        // Initialize the spannable string builders.
+        val requestHeadersBuilder = SpannableStringBuilder()
+        val responseMessageBuilder = SpannableStringBuilder()
+        val responseHeadersBuilder = SpannableStringBuilder()
+        val responseBodyBuilder = SpannableStringBuilder()
+
+        if (urlString.startsWith("content://")) {  // This is a content URL.
+            // Attempt to read the content data.  Return an error if this fails.
+            try {
+                // Get a URI for the content URL.
+                val contentUri = Uri.parse(urlString)
+
+                // Get a cursor with metadata about the content URL.
+                val contentCursor = contentResolver.query(contentUri, null, null, null, null)!!
+
+                // Move the content cursor to the first row.
+                contentCursor.moveToFirst()
+
+                // Populate the response header.
+                for (i in 0 until contentCursor.columnCount) {
+                    // Add a new line if this is not the first entry.
+                    if (i > 0)
+                        responseHeadersBuilder.append(System.getProperty("line.separator"))
+
+                    // Add each header to the string builder.
+                    responseHeadersBuilder.append(contentCursor.getColumnName(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                    responseHeadersBuilder.append(":  ")
+                    responseHeadersBuilder.append(contentCursor.getString(i))
+                }
+
+                // Close the content cursor.
+                contentCursor.close()
+
+                // Create a buffered string reader for the content data.
+                val bufferedReader = BufferedReader(InputStreamReader(contentResolver.openInputStream(contentUri)))
+
+                // Create a buffered string reader for the content data.
+                var contentLineString: String?
+
+                // Get the data from the buffered reader one line at a time.
+                while (bufferedReader.readLine().also { contentLineString = it } != null) {
+                    // Add the line to the response body builder.
+                    responseBodyBuilder.append(contentLineString)
+
+                    // Append a new line.
+                    responseBodyBuilder.append("\n")
+                }
+            } catch (exception: Exception) {
+                // Return the error message.
+                headersViewModel.returnError(exception.toString())
+            }
+        } else {  // This is not a content URL.
+            // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
+            try {
+                // Get the current URL from the main activity.
+                val url = URL(urlString)
+
+                // Open a connection to the URL.  No data is actually sent at this point.
+                val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
+
+                // Set the `Host` header property.
+                httpUrlConnection.setRequestProperty("Host", url.host)
+
+                // Add the `Host` header to the string builder and format the text.
+                requestHeadersBuilder.append("Host", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ")
+                requestHeadersBuilder.append(url.host)
+
+
+                // Set the `Connection` header property.
+                httpUrlConnection.setRequestProperty("Connection", "keep-alive")
+
+                // Add the `Connection` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Connection", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  keep-alive")
+
+
+                // Set the `Upgrade-Insecure-Requests` header property.
+                httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1")
+
+                // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Upgrade-Insecure-Requests", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  1")
+
+
+                // Set the `User-Agent` header property.
+                httpUrlConnection.setRequestProperty("User-Agent", userAgent)
+
+                // Add the `User-Agent` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("User-Agent", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ")
+                requestHeadersBuilder.append(userAgent)
+
+
+                // Set the `Sec-Fetch-Site` header property.
+                httpUrlConnection.setRequestProperty("Sec-Fetch-Site", "none")
+
+                // Add the `Sec-Fetch-Site` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Sec-Fetch-Site", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  none")
+
+
+                // Set the `Sec-Fetch-Mode` header property.
+                httpUrlConnection.setRequestProperty("Sec-Fetch-Mode", "navigate")
+
+                // Add the `Sec-Fetch-Mode` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Sec-Fetch-Mode", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  navigate")
+
+
+                // Set the `Sec-Fetch-User` header property.
+                httpUrlConnection.setRequestProperty("Sec-Fetch-User", "?1")
+
+                // Add the `Sec-Fetch-User` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Sec-Fetch-User", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ?1")
+
+
+                // Set the `Accept` header property.
+                httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
+
+                // Add the `Accept` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Accept", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ")
+                requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
+
+
+                // Set the `Accept-Language` header property.
+                httpUrlConnection.setRequestProperty("Accept-Language", localeString)
+
+                // Add the `Accept-Language` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Accept-Language", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  ")
+                requestHeadersBuilder.append(localeString)
+
+
+                // Get the cookies for the current domain.
+                val cookiesString = CookieManager.getInstance().getCookie(url.toString())
+
+                // Only process the cookies if they are not null.
+                if (cookiesString != null) {
+                    // Add the cookies to the header property.
+                    httpUrlConnection.setRequestProperty("Cookie", cookiesString)
+
+                    // Add the cookie header to the string builder and format the text.
+                    requestHeadersBuilder.append(System.getProperty("line.separator"))
+                    requestHeadersBuilder.append("Cookie", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                    requestHeadersBuilder.append(":  ")
+                    requestHeadersBuilder.append(cookiesString)
+                }
+
+
+                // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default.  If the property is manually set, than `HttpUrlConnection` does not process the decoding.
+                // Add the `Accept-Encoding` header to the string builder and format the text.
+                requestHeadersBuilder.append(System.getProperty("line.separator"))
+                requestHeadersBuilder.append("Accept-Encoding", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                requestHeadersBuilder.append(":  gzip")
+
+                // Ignore SSL errors if requested.
+                if (ignoreSslErrors) {
+                    // Create a new host name verifier that allows all host names without checking for SSL errors.
+                    val hostnameVerifier = HostnameVerifier { _: String?, _: SSLSession? -> true }
+
+                    // Create a new trust manager.  Lint wants to warn us that it is hard to securely implement an X509 trust manager.
+                    // But the point of this trust manager is that it should accept all certificates no matter what, so that isn't an issue in our case.
+                    @SuppressLint("CustomX509TrustManager") val trustManager = arrayOf<TrustManager>(
+                        object : X509TrustManager {
+                            @SuppressLint("TrustAllX509TrustManager")
+                            override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
+                                // Do nothing, which trusts all client certificates.
+                            }
+
+                            @SuppressLint("TrustAllX509TrustManager")
+                            override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
+                                // Do nothing, which trusts all server certificates.
+                            }
+
+                            override fun getAcceptedIssuers(): Array<X509Certificate>? {
+                                return null
+                            }
+                        }
+                    )
+
+                    // Get an SSL context.  `TLS` provides a base instance available from API 1.  <https://developer.android.com/reference/javax/net/ssl/SSLContext>
+                    val sslContext = SSLContext.getInstance("TLS")
+
+                    // Initialize the SSL context with the blank trust manager.
+                    sslContext.init(null, trustManager, SecureRandom())
+
+                    // Get the SSL socket factory with the blank trust manager.
+                    val socketFactory = sslContext.socketFactory
+
+                    // Set the HTTPS URL Connection to use the blank host name verifier.
+                    (httpUrlConnection as HttpsURLConnection).hostnameVerifier = hostnameVerifier
+
+                    // Set the HTTPS URL connection to use the socket factory with the blank trust manager.
+                    httpUrlConnection.sslSocketFactory = socketFactory
+                }
+
+                // 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.
+                try {
+                    // Get the response code, which causes the connection to the server to be made.
+                    val responseCode = httpUrlConnection.responseCode
+
+                    // Populate the response message string builder.
+                    responseMessageBuilder.append(responseCode.toString(), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                    responseMessageBuilder.append(":  ")
+                    responseMessageBuilder.append(httpUrlConnection.responseMessage)
+
+                    // Initialize the iteration variable.
+                    var i = 0
+
+                    // Iterate through the received header fields.
+                    while (httpUrlConnection.getHeaderField(i) != null) {
+                        // Add a new line if there is already information in the string builder.
+                        if (i > 0)
+                            responseHeadersBuilder.append(System.getProperty("line.separator"))
+
+                        // Add the header to the string builder and format the text.
+                        responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+                        responseHeadersBuilder.append(":  ")
+                        responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i))
+
+                        // Increment the iteration variable.
+                        i++
+                    }
+
+                    // Get the correct input stream based on the response code.
+                    val inputStream: InputStream = if (responseCode == 404)  // Get the error stream.
+                        BufferedInputStream(httpUrlConnection.errorStream)
+                    else  // Get the response body stream.
+                        BufferedInputStream(httpUrlConnection.inputStream)
+
+                    // Initialize the byte array output stream and the conversion buffer byte array.
+                    val byteArrayOutputStream = ByteArrayOutputStream()
+                    val conversionBufferByteArray = ByteArray(1024)
+
+                    // Define the buffer length variable.
+                    var bufferLength: Int
+
+                    try {
+                        // Attempt to read data from the input stream and store it in the conversion buffer byte array.  Also store the amount of data read in the buffer length variable.
+                        while (inputStream.read(conversionBufferByteArray).also { bufferLength = it } > 0) {  // Proceed while the amount of data stored in the buffer is > 0.
+                            // Write the contents of the conversion buffer to the byte array output stream.
+                            byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength)
+                        }
+                    } catch (exception: IOException) {
+                        // Return the error message.
+                        headersViewModel.returnError(exception.toString())
+                    }
+
+                    // Close the input stream.
+                    inputStream.close()
+
+                    // Populate the response body string with the contents of the byte array output stream.
+                    responseBodyBuilder.append(byteArrayOutputStream.toString())
+                } finally {
+                    // Disconnect HTTP URL connection.
+                    httpUrlConnection.disconnect()
+                }
+            } catch (exception: Exception) {
+                // Return the error message.
+                headersViewModel.returnError(exception.toString())
+            }
+        }
+
+        // Return the spannable string builders.
+        return arrayOf(requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder)
+    }
+}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt b/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt
deleted file mode 100644 (file)
index f7ad0a5..0000000
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * Copyright © 2017-2023 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.backgroundtasks
-
-import android.annotation.SuppressLint
-import android.content.ContentResolver
-import android.graphics.Typeface
-import android.net.Uri
-import android.text.SpannableStringBuilder
-import android.text.Spanned
-import android.text.style.StyleSpan
-import android.webkit.CookieManager
-
-import com.stoutner.privacybrowser.viewmodels.WebViewSource
-
-import java.io.BufferedInputStream
-import java.io.BufferedReader
-import java.io.ByteArrayOutputStream
-import java.io.IOException
-import java.io.InputStream
-import java.io.InputStreamReader
-
-import java.net.HttpURLConnection
-import java.net.Proxy
-import java.net.URL
-
-import java.security.SecureRandom
-import java.security.cert.X509Certificate
-
-import javax.net.ssl.HostnameVerifier
-import javax.net.ssl.HttpsURLConnection
-import javax.net.ssl.SSLContext
-import javax.net.ssl.SSLSession
-import javax.net.ssl.TrustManager
-import javax.net.ssl.X509TrustManager
-
-
-class GetSourceBackgroundTask {
-    fun acquire(urlString: String, userAgent: String, localeString: String, proxy: Proxy, contentResolver: ContentResolver, webViewSource: WebViewSource, ignoreSslErrors: Boolean):
-            Array<SpannableStringBuilder> {
-
-        // Initialize the spannable string builders.
-        val requestHeadersBuilder = SpannableStringBuilder()
-        val responseMessageBuilder = SpannableStringBuilder()
-        val responseHeadersBuilder = SpannableStringBuilder()
-        val responseBodyBuilder = SpannableStringBuilder()
-
-        if (urlString.startsWith("content://")) {  // This is a content URL.
-            // Attempt to read the content data.  Return an error if this fails.
-            try {
-                // Get a URI for the content URL.
-                val contentUri = Uri.parse(urlString)
-
-                // Get a cursor with metadata about the content URL.
-                val contentCursor = contentResolver.query(contentUri, null, null, null, null)!!
-
-                // Move the content cursor to the first row.
-                contentCursor.moveToFirst()
-
-                // Populate the response header.
-                for (i in 0 until contentCursor.columnCount) {
-                    // Add a new line if this is not the first entry.
-                    if (i > 0)
-                        responseHeadersBuilder.append(System.getProperty("line.separator"))
-
-                    // Add each header to the string builder.
-                    responseHeadersBuilder.append(contentCursor.getColumnName(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                    responseHeadersBuilder.append(":  ")
-                    responseHeadersBuilder.append(contentCursor.getString(i))
-                }
-
-                // Close the content cursor.
-                contentCursor.close()
-
-                // Create a buffered string reader for the content data.
-                val bufferedReader = BufferedReader(InputStreamReader(contentResolver.openInputStream(contentUri)))
-
-                // Create a buffered string reader for the content data.
-                var contentLineString: String?
-
-                // Get the data from the buffered reader one line at a time.
-                while (bufferedReader.readLine().also { contentLineString = it } != null) {
-                    // Add the line to the response body builder.
-                    responseBodyBuilder.append(contentLineString)
-
-                    // Append a new line.
-                    responseBodyBuilder.append("\n")
-                }
-            } catch (exception: Exception) {
-                // Return the error message.
-                webViewSource.returnError(exception.toString())
-            }
-        } else {  // This is not a content URL.
-            // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
-            try {
-                // Get the current URL from the main activity.
-                val url = URL(urlString)
-
-                // Open a connection to the URL.  No data is actually sent at this point.
-                val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
-
-                // Set the `Host` header property.
-                httpUrlConnection.setRequestProperty("Host", url.host)
-
-                // Add the `Host` header to the string builder and format the text.
-                requestHeadersBuilder.append("Host", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  ")
-                requestHeadersBuilder.append(url.host)
-
-
-                // Set the `Connection` header property.
-                httpUrlConnection.setRequestProperty("Connection", "keep-alive")
-
-                // Add the `Connection` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"))
-                requestHeadersBuilder.append("Connection", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  keep-alive")
-
-
-                // Set the `Upgrade-Insecure-Requests` header property.
-                httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1")
-
-                // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"))
-                requestHeadersBuilder.append("Upgrade-Insecure-Requests", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  1")
-
-
-                // Set the `User-Agent` header property.
-                httpUrlConnection.setRequestProperty("User-Agent", userAgent)
-
-                // Add the `User-Agent` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"))
-                requestHeadersBuilder.append("User-Agent", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  ")
-                requestHeadersBuilder.append(userAgent)
-
-
-                // Set the `Sec-Fetch-Site` header property.
-                httpUrlConnection.setRequestProperty("Sec-Fetch-Site", "none")
-
-                // Add the `Sec-Fetch-Site` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"))
-                requestHeadersBuilder.append("Sec-Fetch-Site", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  none")
-
-
-                // Set the `Sec-Fetch-Mode` header property.
-                httpUrlConnection.setRequestProperty("Sec-Fetch-Mode", "navigate")
-
-                // Add the `Sec-Fetch-Mode` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"))
-                requestHeadersBuilder.append("Sec-Fetch-Mode", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  navigate")
-
-
-                // Set the `Sec-Fetch-User` header property.
-                httpUrlConnection.setRequestProperty("Sec-Fetch-User", "?1")
-
-                // Add the `Sec-Fetch-User` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"))
-                requestHeadersBuilder.append("Sec-Fetch-User", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  ?1")
-
-
-                // Set the `Accept` header property.
-                httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
-
-                // Add the `Accept` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"))
-                requestHeadersBuilder.append("Accept", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  ")
-                requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
-
-
-                // Set the `Accept-Language` header property.
-                httpUrlConnection.setRequestProperty("Accept-Language", localeString)
-
-                // Add the `Accept-Language` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"))
-                requestHeadersBuilder.append("Accept-Language", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  ")
-                requestHeadersBuilder.append(localeString)
-
-
-                // Get the cookies for the current domain.
-                val cookiesString = CookieManager.getInstance().getCookie(url.toString())
-
-                // Only process the cookies if they are not null.
-                if (cookiesString != null) {
-                    // Add the cookies to the header property.
-                    httpUrlConnection.setRequestProperty("Cookie", cookiesString)
-
-                    // Add the cookie header to the string builder and format the text.
-                    requestHeadersBuilder.append(System.getProperty("line.separator"))
-                    requestHeadersBuilder.append("Cookie", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                    requestHeadersBuilder.append(":  ")
-                    requestHeadersBuilder.append(cookiesString)
-                }
-
-
-                // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default.  If the property is manually set, than `HttpUrlConnection` does not process the decoding.
-                // Add the `Accept-Encoding` header to the string builder and format the text.
-                requestHeadersBuilder.append(System.getProperty("line.separator"))
-                requestHeadersBuilder.append("Accept-Encoding", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                requestHeadersBuilder.append(":  gzip")
-
-                // Ignore SSL errors if requested.
-                if (ignoreSslErrors) {
-                    // Create a new host name verifier that allows all host names without checking for SSL errors.
-                    val hostnameVerifier = HostnameVerifier { _: String?, _: SSLSession? -> true }
-
-                    // Create a new trust manager.  Lint wants to warn us that it is hard to securely implement an X509 trust manager.
-                    // But the point of this trust manager is that it should accept all certificates no matter what, so that isn't an issue in our case.
-                    @SuppressLint("CustomX509TrustManager") val trustManager = arrayOf<TrustManager>(
-                        object : X509TrustManager {
-                            @SuppressLint("TrustAllX509TrustManager")
-                            override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
-                                // Do nothing, which trusts all client certificates.
-                            }
-
-                            @SuppressLint("TrustAllX509TrustManager")
-                            override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
-                                // Do nothing, which trusts all server certificates.
-                            }
-
-                            override fun getAcceptedIssuers(): Array<X509Certificate>? {
-                                return null
-                            }
-                        }
-                    )
-
-                    // Get an SSL context.  `TLS` provides a base instance available from API 1.  <https://developer.android.com/reference/javax/net/ssl/SSLContext>
-                    val sslContext = SSLContext.getInstance("TLS")
-
-                    // Initialize the SSL context with the blank trust manager.
-                    sslContext.init(null, trustManager, SecureRandom())
-
-                    // Get the SSL socket factory with the blank trust manager.
-                    val socketFactory = sslContext.socketFactory
-
-                    // Set the HTTPS URL Connection to use the blank host name verifier.
-                    (httpUrlConnection as HttpsURLConnection).hostnameVerifier = hostnameVerifier
-
-                    // Set the HTTPS URL connection to use the socket factory with the blank trust manager.
-                    httpUrlConnection.sslSocketFactory = socketFactory
-                }
-
-                // 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.
-                try {
-                    // Get the response code, which causes the connection to the server to be made.
-                    val responseCode = httpUrlConnection.responseCode
-
-                    // Populate the response message string builder.
-                    responseMessageBuilder.append(responseCode.toString(), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                    responseMessageBuilder.append(":  ")
-                    responseMessageBuilder.append(httpUrlConnection.responseMessage)
-
-                    // Initialize the iteration variable.
-                    var i = 0
-
-                    // Iterate through the received header fields.
-                    while (httpUrlConnection.getHeaderField(i) != null) {
-                        // Add a new line if there is already information in the string builder.
-                        if (i > 0)
-                            responseHeadersBuilder.append(System.getProperty("line.separator"))
-
-                        // Add the header to the string builder and format the text.
-                        responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
-                        responseHeadersBuilder.append(":  ")
-                        responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i))
-
-                        // Increment the iteration variable.
-                        i++
-                    }
-
-                    // Get the correct input stream based on the response code.
-                    val inputStream: InputStream = if (responseCode == 404)  // Get the error stream.
-                        BufferedInputStream(httpUrlConnection.errorStream)
-                    else  // Get the response body stream.
-                        BufferedInputStream(httpUrlConnection.inputStream)
-
-                    // Initialize the byte array output stream and the conversion buffer byte array.
-                    val byteArrayOutputStream = ByteArrayOutputStream()
-                    val conversionBufferByteArray = ByteArray(1024)
-
-                    // Define the buffer length variable.
-                    var bufferLength: Int
-
-                    try {
-                        // Attempt to read data from the input stream and store it in the conversion buffer byte array.  Also store the amount of data read in the buffer length variable.
-                        while (inputStream.read(conversionBufferByteArray).also { bufferLength = it } > 0) {  // Proceed while the amount of data stored in the buffer is > 0.
-                            // Write the contents of the conversion buffer to the byte array output stream.
-                            byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength)
-                        }
-                    } catch (exception: IOException) {
-                        // Return the error message.
-                        webViewSource.returnError(exception.toString())
-                    }
-
-                    // Close the input stream.
-                    inputStream.close()
-
-                    // Populate the response body string with the contents of the byte array output stream.
-                    responseBodyBuilder.append(byteArrayOutputStream.toString())
-                } finally {
-                    // Disconnect HTTP URL connection.
-                    httpUrlConnection.disconnect()
-                }
-            } catch (exception: Exception) {
-                // Return the error message.
-                webViewSource.returnError(exception.toString())
-            }
-        }
-
-        // Return the spannable string builders.
-        return arrayOf(requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder)
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/AboutViewHeadersDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/AboutViewHeadersDialog.kt
new file mode 100644 (file)
index 0000000..f88da7b
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.dialogs
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.WindowManager
+
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.preference.PreferenceManager
+
+import com.stoutner.privacybrowser.R
+
+class AboutViewHeadersDialog : DialogFragment() {
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        // Use a builder to create the alert dialog.
+        val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
+
+        // Set the icon according to the theme.
+        dialogBuilder.setIcon(R.drawable.about_blue)
+
+        // Set the title.
+        dialogBuilder.setTitle(R.string.about_view_headers)
+
+        // Set the text.
+        dialogBuilder.setMessage(R.string.about_view_headers_message)
+
+        // Set the close button listener.  Using `null` as the listener closes the dialog without doing anything else.
+        dialogBuilder.setNegativeButton(R.string.close, null)
+
+        // Create an alert dialog from the alert dialog builder.
+        val alertDialog = dialogBuilder.create()
+
+        // Get a handle for the shared preferences.
+        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
+
+        // Get the screenshot preference.
+        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+
+        // Disable screenshots if not allowed.
+        if (!allowScreenshots) {
+            alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        }
+
+        // Return the alert dialog.
+        return alertDialog
+    }
+}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/AboutViewSourceDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/AboutViewSourceDialog.kt
deleted file mode 100644 (file)
index 8ac8331..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright © 2018-2022 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.dialogs
-
-import android.app.Dialog
-import android.os.Bundle
-import android.view.WindowManager
-
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.DialogFragment
-import androidx.preference.PreferenceManager
-
-import com.stoutner.privacybrowser.R
-
-class AboutViewSourceDialog : DialogFragment() {
-    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-        // Use a builder to create the alert dialog.
-        val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
-
-        // Set the icon according to the theme.
-        dialogBuilder.setIcon(R.drawable.about_blue)
-
-        // Set the title.
-        dialogBuilder.setTitle(R.string.about_view_source)
-
-        // Set the text.
-        dialogBuilder.setMessage(R.string.about_view_source_message)
-
-        // Set the close button listener.  Using `null` as the listener closes the dialog without doing anything else.
-        dialogBuilder.setNegativeButton(R.string.close, null)
-
-        // Create an alert dialog from the alert dialog builder.
-        val alertDialog = dialogBuilder.create()
-
-        // Get a handle for the shared preferences.
-        val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
-
-        // Get the screenshot preference.
-        val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
-
-        // Disable screenshots if not allowed.
-        if (!allowScreenshots) {
-            alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
-        }
-
-        // Return the alert dialog.
-        return alertDialog
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/ViewHeadersFactory.kt b/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/ViewHeadersFactory.kt
new file mode 100644 (file)
index 0000000..83f0a36
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.viewmodelfactories
+
+import android.content.ContentResolver
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+
+import java.net.Proxy
+import java.util.concurrent.ExecutorService
+
+class ViewHeadersFactory (private val urlString: String, private val userAgent: String, private val localeString: String, private val proxy: Proxy, private val contentResolver: ContentResolver,
+                          private val executorService: ExecutorService): ViewModelProvider.Factory {
+    // Override the create function in order to add the provided arguments.
+    override fun <T: ViewModel> create(modelClass: Class<T>): T {
+        // Return a new instance of the model class with the provided arguments.
+        return modelClass.getConstructor(String::class.java, String::class.java, String::class.java, Proxy::class.java, ContentResolver::class.java, ExecutorService::class.java)
+                .newInstance(urlString, userAgent, localeString, proxy, contentResolver, executorService)
+    }
+}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/WebViewSourceFactory.kt b/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/WebViewSourceFactory.kt
deleted file mode 100644 (file)
index f8de88f..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright © 2020-2022 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.viewmodelfactories
-
-import android.content.ContentResolver
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-
-import java.net.Proxy
-import java.util.concurrent.ExecutorService
-
-class WebViewSourceFactory (private val urlString: String, private val userAgent: String, private val localeString: String, private val proxy: Proxy, private val contentResolver: ContentResolver,
-                            private val executorService: ExecutorService): ViewModelProvider.Factory {
-    // Override the create function in order to add the provided arguments.
-    override fun <T: ViewModel> create(modelClass: Class<T>): T {
-        // Return a new instance of the model class with the provided arguments.
-        return modelClass.getConstructor(String::class.java, String::class.java, String::class.java, Proxy::class.java, ContentResolver::class.java, ExecutorService::class.java)
-                .newInstance(urlString, userAgent, localeString, proxy, contentResolver, executorService)
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/stoutner/privacybrowser/viewmodels/HeadersViewModel.kt b/app/src/main/java/com/stoutner/privacybrowser/viewmodels/HeadersViewModel.kt
new file mode 100644 (file)
index 0000000..ccfe79a
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020-2023 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+ *
+ * Privacy Browser Android is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser Android is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.viewmodels
+
+import android.content.ContentResolver
+import android.text.SpannableStringBuilder
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+import com.stoutner.privacybrowser.backgroundtasks.GetHeadersBackgroundTask
+
+import java.net.Proxy
+import java.util.concurrent.ExecutorService
+
+class HeadersViewModel(private val urlString: String, private val userAgent: String, private val localeString: String, private val proxy: Proxy, private val contentResolver: ContentResolver,
+                       private val executorService: ExecutorService): ViewModel() {
+    // Initialize the mutable live data variables.
+    private val mutableLiveDataSourceStringArray = MutableLiveData<Array<SpannableStringBuilder>>()
+    private val mutableLiveDataErrorString = MutableLiveData<String>()
+
+    // Initialize the view model.
+    init {
+        // Instantiate the get headers background task class.
+        val getSourceBackgroundTask = GetHeadersBackgroundTask()
+
+        // Get the headers.
+        executorService.execute { mutableLiveDataSourceStringArray.postValue(getSourceBackgroundTask.acquire(urlString, userAgent, localeString, proxy, contentResolver, this,
+            false)) }
+    }
+
+    // The headers observer.
+    fun observeHeaders(): LiveData<Array<SpannableStringBuilder>> {
+        // Return the source to the activity.
+        return mutableLiveDataSourceStringArray
+    }
+
+    // The error observer.
+    fun observeErrors(): LiveData<String> {
+        // Return any errors to the activity.
+        return mutableLiveDataErrorString
+    }
+
+    // The interface for returning the error from the background task
+    fun returnError(errorString: String) {
+        // Update the mutable live data error string.
+        mutableLiveDataErrorString.postValue(errorString)
+    }
+
+    // The workhorse that gets the headers.
+    fun updateHeaders(urlString: String, ignoreSslErrors: Boolean) {
+        // Reset the mutable live data error string.  This prevents the snackbar from displaying later if the activity restarts.
+        mutableLiveDataErrorString.postValue("")
+
+        // Instantiate the get headers background task class.
+        val getSourceBackgroundTask = GetHeadersBackgroundTask()
+
+        // Get the headers.
+        executorService.execute { mutableLiveDataSourceStringArray.postValue(getSourceBackgroundTask.acquire(urlString, userAgent, localeString, proxy, contentResolver, this,
+            ignoreSslErrors)) }
+    }
+}
diff --git a/app/src/main/java/com/stoutner/privacybrowser/viewmodels/WebViewSource.kt b/app/src/main/java/com/stoutner/privacybrowser/viewmodels/WebViewSource.kt
deleted file mode 100644 (file)
index d55540e..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright © 2020-2022 Soren Stoutner <soren@stoutner.com>.
- *
- * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
- *
- * Privacy Browser Android is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Privacy Browser Android is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.stoutner.privacybrowser.viewmodels
-
-import android.content.ContentResolver
-import android.text.SpannableStringBuilder
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-
-import com.stoutner.privacybrowser.backgroundtasks.GetSourceBackgroundTask
-
-import java.net.Proxy
-import java.util.concurrent.ExecutorService
-
-class WebViewSource(private val urlString: String, private val userAgent: String, private val localeString: String, private val proxy: Proxy, private val contentResolver: ContentResolver,
-                    private val executorService: ExecutorService): ViewModel() {
-    // Initialize the mutable live data variables.
-    private val mutableLiveDataSourceStringArray = MutableLiveData<Array<SpannableStringBuilder>>()
-    private val mutableLiveDataErrorString = MutableLiveData<String>()
-
-    // Initialize the view model.
-    init {
-        // Instantiate the get source background task class.
-        val getSourceBackgroundTask = GetSourceBackgroundTask()
-
-        // Get the source.
-        executorService.execute { mutableLiveDataSourceStringArray.postValue(getSourceBackgroundTask.acquire(urlString, userAgent, localeString, proxy, contentResolver, this,
-            false)) }
-    }
-
-    // The source observer.
-    fun observeSource(): LiveData<Array<SpannableStringBuilder>> {
-        // Return the source to the activity.
-        return mutableLiveDataSourceStringArray
-    }
-
-    // The error observer.
-    fun observeErrors(): LiveData<String> {
-        // Return any errors to the activity.
-        return mutableLiveDataErrorString
-    }
-
-    // The interface for returning the error from the background task
-    fun returnError(errorString: String) {
-        // Update the mutable live data error string.
-        mutableLiveDataErrorString.postValue(errorString)
-    }
-
-    // The workhorse that gets the source.
-    fun updateSource(urlString: String, ignoreSslErrors: Boolean) {
-        // Reset the mutable live data error string.  This prevents the snackbar from displaying later if the activity restarts.
-        mutableLiveDataErrorString.postValue("")
-
-        // Instantiate the get source background task class.
-        val getSourceBackgroundTask = GetSourceBackgroundTask()
-
-        // Get the source.
-        executorService.execute { mutableLiveDataSourceStringArray.postValue(getSourceBackgroundTask.acquire(urlString, userAgent, localeString, proxy, contentResolver, this,
-            ignoreSslErrors)) }
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_headers_appbar_custom_view.xml b/app/src/main/res/layout/view_headers_appbar_custom_view.xml
new file mode 100644 (file)
index 0000000..17982c0
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright 2015-2020,2022-2023 Soren Stoutner <soren@stoutner.com>.
+
+  This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+
+  Privacy Browser Android is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  Privacy Browser Android is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
+
+<!-- Relative layout is used instead of a linear layout because `supportAppBar` does not let `android:layout_weight="1"` cause the URL text box to fill all the available space. -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent" >
+
+    <ImageView
+        android:id="@+id/back_arrow"
+        android:src="@drawable/back"
+        app:tint="?attr/colorControlNormal"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_marginEnd="14dp"
+        android:contentDescription="@string/back"
+        android:onClick="goBack" />
+
+    <!-- `android:imeOptions="actionGo"` sets the keyboard to have a `go` key instead of a `new line` key.
+        `android:inputType="textUri"` disables spell check in the `EditText`. -->
+    <EditText
+        android:id="@+id/url_edittext"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:layout_toEndOf="@id/back_arrow"
+        android:hint="@string/url"
+        android:imeOptions="actionGo"
+        android:inputType="textUri"
+        android:selectAllOnFocus="true"
+        tools:ignore="Autofill" />
+</RelativeLayout>
diff --git a/app/src/main/res/layout/view_headers_bottom_appbar.xml b/app/src/main/res/layout/view_headers_bottom_appbar.xml
new file mode 100644 (file)
index 0000000..0e05caf
--- /dev/null
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
+
+  This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+
+  Privacy Browser Android is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  Privacy Browser Android is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
+
+<!-- Setting the layout root to be `focusableInTouchMode` prevents the URL toolbar from stealing focus on launch and opening the keyboard. -->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    android:id="@+id/coordinatorlayout"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <!-- The linear layout with `orientation="vertical"` keeps the content above the app bar layout.  `app:layout_dodgeInsetEdges="bottom"` as a child of a coordinator layout moves the view above snackbars. -->
+    <LinearLayout
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:orientation="vertical"
+        app:layout_dodgeInsetEdges="bottom" >
+
+        <!-- `android:layout_weight="1"` causes the swipe refresh layout to fill all the remaining space. -->
+        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+            android:id="@+id/swiperefreshlayout"
+            android:layout_height="0dp"
+            android:layout_width="match_parent"
+            android:layout_weight="1">
+
+            <ScrollView
+                android:id="@+id/scrollview"
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent" >
+
+                <LinearLayout
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:orientation="vertical"
+                    android:layout_margin="10dp" >
+
+                    <!-- Request headers. -->
+                    <TextView
+                        android:id="@+id/request_headers_title_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:text="@string/request_headers"
+                        android:textAlignment="center"
+                        android:textSize="18sp"
+                        android:textColor="@color/blue_text"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/request_headers_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textIsSelectable="true"
+                        android:layout_marginBottom="8dp" />
+
+                    <!-- Response message. -->
+                    <TextView
+                        android:id="@+id/response_message_title_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:text="@string/response_message"
+                        android:textAlignment="center"
+                        android:textSize="18sp"
+                        android:textColor="@color/blue_text"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/response_message_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textIsSelectable="true"
+                        android:layout_marginBottom="8dp" />
+
+                    <!-- Response headers. -->
+                    <!-- The title text is set programatically. -->
+                    <TextView
+                        android:id="@+id/response_headers_title_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textAlignment="center"
+                        android:textSize="18sp"
+                        android:textColor="@color/blue_text"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/response_headers_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textIsSelectable="true"
+                        android:layout_marginBottom="8dp" />
+
+                    <!-- Response body. -->
+                    <!-- The title text is set programatically. -->
+                    <TextView
+                        android:id="@+id/response_body_title_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textAlignment="center"
+                        android:textSize="18sp"
+                        android:textColor="@color/blue_text"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/response_body_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textIsSelectable="true" />
+                </LinearLayout>
+            </ScrollView>
+        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+
+        <!-- The app bar theme must be specified here because the activity uses a `NoActionBar` theme. -->
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+            android:background="?android:attr/colorBackground"
+            android:theme="@style/PrivacyBrowserAppBar" >
+
+            <!-- The frame layout allows the toolbar and the progress bar to occupy the same space. -->
+            <FrameLayout
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent" >
+
+                <androidx.appcompat.widget.Toolbar
+                    android:id="@+id/toolbar"
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent" />
+
+                <!-- Android automatically uses a different, skinnier drawable with padding for indeterminate horizontal progress bars in API >= 21.
+                    They make this very difficult to override.  https://redmine.stoutner.com/issues/241
+                    `tools:ignore="UnusedAttribute"` removes the lint warning about `progressTint` and `progressBackgroundTint` not applying to API < 21. -->
+                <ProgressBar
+                    android:id="@+id/progress_bar"
+                    style="?android:attr/progressBarStyleHorizontal"
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:minHeight="3dp"
+                    android:layout_gravity="bottom"
+                    android:visibility="gone"
+                    tools:ignore="UnusedAttribute" />
+            </FrameLayout>
+        </com.google.android.material.appbar.AppBarLayout>
+    </LinearLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/view_headers_top_appbar.xml b/app/src/main/res/layout/view_headers_top_appbar.xml
new file mode 100644 (file)
index 0000000..3171bbc
--- /dev/null
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
+
+  This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+
+  Privacy Browser Android is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  Privacy Browser Android is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
+
+<!-- Setting the layout root to be `focusableInTouchMode` prevents the URL toolbar from stealing focus on launch and opening the keyboard. -->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    android:id="@+id/coordinatorlayout"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true" >
+
+    <!-- The linear layout with `orientation="vertical"` moves the content below the app bar layout. -->
+    <LinearLayout
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:orientation="vertical" >
+
+        <!-- The app bar theme must be specified here because the activity uses a `NoActionBar` theme. -->
+        <com.google.android.material.appbar.AppBarLayout
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+            android:background="?android:attr/colorBackground"
+            android:theme="@style/PrivacyBrowserAppBar" >
+
+            <!-- The frame layout allows the toolbar and the progress bar to occupy the same space. -->
+            <FrameLayout
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent" >
+
+                <androidx.appcompat.widget.Toolbar
+                    android:id="@+id/toolbar"
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent" />
+
+                <!-- Android automatically uses a different, skinnier drawable with padding for indeterminate horizontal progress bars in API >= 21.
+                    They make this very difficult to override.  https://redmine.stoutner.com/issues/241
+                    `tools:ignore="UnusedAttribute"` removes the lint warning about `progressTint` and `progressBackgroundTint` not applying to API < 21. -->
+                <ProgressBar
+                    android:id="@+id/progress_bar"
+                    style="?android:attr/progressBarStyleHorizontal"
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:minHeight="3dp"
+                    android:layout_gravity="bottom"
+                    android:visibility="gone"
+                    tools:ignore="UnusedAttribute" />
+            </FrameLayout>
+        </com.google.android.material.appbar.AppBarLayout>
+
+        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+            android:id="@+id/swiperefreshlayout"
+            android:layout_height="match_parent"
+            android:layout_width="match_parent">
+
+            <ScrollView
+                android:id="@+id/view_source_scrollview"
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent" >
+
+                <LinearLayout
+                    android:layout_height="wrap_content"
+                    android:layout_width="match_parent"
+                    android:orientation="vertical"
+                    android:layout_margin="10dp" >
+
+                    <!-- Request headers. -->
+                    <TextView
+                        android:id="@+id/request_headers_title_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:text="@string/request_headers"
+                        android:textAlignment="center"
+                        android:textSize="18sp"
+                        android:textColor="@color/blue_text"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/request_headers_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textIsSelectable="true"
+                        android:layout_marginBottom="8dp" />
+
+                    <!-- Response message. -->
+                    <TextView
+                        android:id="@+id/response_message_title_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:text="@string/response_message"
+                        android:textAlignment="center"
+                        android:textSize="18sp"
+                        android:textColor="@color/blue_text"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/response_message_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textIsSelectable="true"
+                        android:layout_marginBottom="8dp" />
+
+                    <!-- Response headers. -->
+                    <!-- The title text is set programatically. -->
+                    <TextView
+                        android:id="@+id/response_headers_title_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textAlignment="center"
+                        android:textSize="18sp"
+                        android:textColor="@color/blue_text"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/response_headers_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textIsSelectable="true"
+                        android:layout_marginBottom="8dp" />
+
+                    <!-- Response body. -->
+                    <!-- The title text is set programatically. -->
+                    <TextView
+                        android:id="@+id/response_body_title_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textAlignment="center"
+                        android:textSize="18sp"
+                        android:textColor="@color/blue_text"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/response_body_textview"
+                        android:layout_height="wrap_content"
+                        android:layout_width="match_parent"
+                        android:textIsSelectable="true" />
+                </LinearLayout>
+            </ScrollView>
+        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+    </LinearLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/view_source_appbar_custom_view.xml b/app/src/main/res/layout/view_source_appbar_custom_view.xml
deleted file mode 100644 (file)
index 83a465c..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  Copyright © 2015-2020,2022 Soren Stoutner <soren@stoutner.com>.
-
-  This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
-
-  Privacy Browser Android is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License as published by
-  the Free Software Foundation, either version 3 of the License, or
-  (at your option) any later version.
-
-  Privacy Browser Android is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
-
-<!-- Relative layout is used instead of a linear layout because `supportAppBar` does not let `android:layout_weight="1"` cause the URL text box to fill all the available space. -->
-<RelativeLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_height="wrap_content"
-    android:layout_width="match_parent" >
-
-    <ImageView
-        android:id="@+id/back_arrow"
-        android:src="@drawable/back"
-        app:tint="?attr/colorControlNormal"
-        android:layout_height="wrap_content"
-        android:layout_width="wrap_content"
-        android:layout_centerVertical="true"
-        android:layout_marginEnd="14dp"
-        android:contentDescription="@string/back"
-        android:onClick="goBack" />
-
-    <!-- `android:imeOptions="actionGo"` sets the keyboard to have a `go` key instead of a `new line` key.
-        `android:inputType="textUri"` disables spell check in the `EditText`. -->
-    <EditText
-        android:id="@+id/url_edittext"
-        android:layout_height="wrap_content"
-        android:layout_width="match_parent"
-        android:layout_toEndOf="@id/back_arrow"
-        android:hint="@string/url"
-        android:imeOptions="actionGo"
-        android:inputType="textUri"
-        android:selectAllOnFocus="true"
-        tools:ignore="Autofill" />
-</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_source_bottom_appbar.xml b/app/src/main/res/layout/view_source_bottom_appbar.xml
deleted file mode 100644 (file)
index 852d1c0..0000000
+++ /dev/null
@@ -1,163 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  Copyright 2017-2022 Soren Stoutner <soren@stoutner.com>.
-
-  This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
-
-  Privacy Browser Android is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License as published by
-  the Free Software Foundation, either version 3 of the License, or
-  (at your option) any later version.
-
-  Privacy Browser Android is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
-
-<!-- Setting the layout root to be `focusableInTouchMode` prevents the URL toolbar from stealing focus on launch and opening the keyboard. -->
-<androidx.coordinatorlayout.widget.CoordinatorLayout
-    android:id="@+id/view_source_coordinatorlayout"
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_height="match_parent"
-    android:layout_width="match_parent"
-    android:focusable="true"
-    android:focusableInTouchMode="true"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <!-- The linear layout with `orientation="vertical"` keeps the content above the app bar layout.  `app:layout_dodgeInsetEdges="bottom"` as a child of a coordinator layout moves the view above snackbars. -->
-    <LinearLayout
-        android:layout_height="match_parent"
-        android:layout_width="match_parent"
-        android:orientation="vertical"
-        app:layout_dodgeInsetEdges="bottom" >
-
-        <!-- `android:layout_weight="1"` causes the swipe refresh layout to fill all the remaining space. -->
-        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-            android:id="@+id/view_source_swiperefreshlayout"
-            android:layout_height="0dp"
-            android:layout_width="match_parent"
-            android:layout_weight="1">
-
-            <ScrollView
-                android:id="@+id/view_source_scrollview"
-                android:layout_height="wrap_content"
-                android:layout_width="match_parent" >
-
-                <LinearLayout
-                    android:layout_height="wrap_content"
-                    android:layout_width="match_parent"
-                    android:orientation="vertical"
-                    android:layout_margin="10dp" >
-
-                    <!-- Request headers. -->
-                    <TextView
-                        android:id="@+id/request_headers_title_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:text="@string/request_headers"
-                        android:textAlignment="center"
-                        android:textSize="18sp"
-                        android:textColor="@color/blue_text"
-                        android:textStyle="bold" />
-
-                    <TextView
-                        android:id="@+id/request_headers_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textIsSelectable="true"
-                        android:layout_marginBottom="8dp" />
-
-                    <!-- Response message. -->
-                    <TextView
-                        android:id="@+id/response_message_title_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:text="@string/response_message"
-                        android:textAlignment="center"
-                        android:textSize="18sp"
-                        android:textColor="@color/blue_text"
-                        android:textStyle="bold" />
-
-                    <TextView
-                        android:id="@+id/response_message_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textIsSelectable="true"
-                        android:layout_marginBottom="8dp" />
-
-                    <!-- Response headers. -->
-                    <!-- The title text is set programatically. -->
-                    <TextView
-                        android:id="@+id/response_headers_title_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textAlignment="center"
-                        android:textSize="18sp"
-                        android:textColor="@color/blue_text"
-                        android:textStyle="bold" />
-
-                    <TextView
-                        android:id="@+id/response_headers_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textIsSelectable="true"
-                        android:layout_marginBottom="8dp" />
-
-                    <!-- Response body. -->
-                    <!-- The title text is set programatically. -->
-                    <TextView
-                        android:id="@+id/response_body_title_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textAlignment="center"
-                        android:textSize="18sp"
-                        android:textColor="@color/blue_text"
-                        android:textStyle="bold" />
-
-                    <TextView
-                        android:id="@+id/response_body_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textIsSelectable="true" />
-                </LinearLayout>
-            </ScrollView>
-        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
-
-        <!-- The app bar theme must be specified here because the activity uses a `NoActionBar` theme. -->
-        <com.google.android.material.appbar.AppBarLayout
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-            android:background="?android:attr/colorBackground"
-            android:theme="@style/PrivacyBrowserAppBar" >
-
-            <!-- The frame layout allows the toolbar and the progress bar to occupy the same space. -->
-            <FrameLayout
-                android:layout_height="wrap_content"
-                android:layout_width="match_parent" >
-
-                <androidx.appcompat.widget.Toolbar
-                    android:id="@+id/view_source_toolbar"
-                    android:layout_height="wrap_content"
-                    android:layout_width="match_parent" />
-
-                <!-- Android automatically uses a different, skinnier drawable with padding for indeterminate horizontal progress bars in API >= 21.
-                    They make this very difficult to override.  https://redmine.stoutner.com/issues/241
-                    `tools:ignore="UnusedAttribute"` removes the lint warning about `progressTint` and `progressBackgroundTint` not applying to API < 21. -->
-                <ProgressBar
-                    android:id="@+id/progress_bar"
-                    style="?android:attr/progressBarStyleHorizontal"
-                    android:layout_height="wrap_content"
-                    android:layout_width="match_parent"
-                    android:minHeight="3dp"
-                    android:layout_gravity="bottom"
-                    android:visibility="gone"
-                    tools:ignore="UnusedAttribute" />
-            </FrameLayout>
-        </com.google.android.material.appbar.AppBarLayout>
-    </LinearLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/view_source_top_appbar.xml b/app/src/main/res/layout/view_source_top_appbar.xml
deleted file mode 100644 (file)
index a63e941..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  Copyright 2017-2022 Soren Stoutner <soren@stoutner.com>.
-
-  This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
-
-  Privacy Browser Android is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License as published by
-  the Free Software Foundation, either version 3 of the License, or
-  (at your option) any later version.
-
-  Privacy Browser Android is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
-
-<!-- Setting the layout root to be `focusableInTouchMode` prevents the URL toolbar from stealing focus on launch and opening the keyboard. -->
-<androidx.coordinatorlayout.widget.CoordinatorLayout
-    android:id="@+id/view_source_coordinatorlayout"
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_height="match_parent"
-    android:layout_width="match_parent"
-    android:focusable="true"
-    android:focusableInTouchMode="true" >
-
-    <!-- The linear layout with `orientation="vertical"` moves the content below the app bar layout. -->
-    <LinearLayout
-        android:layout_height="match_parent"
-        android:layout_width="match_parent"
-        android:orientation="vertical" >
-
-        <!-- The app bar theme must be specified here because the activity uses a `NoActionBar` theme. -->
-        <com.google.android.material.appbar.AppBarLayout
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-            android:background="?android:attr/colorBackground"
-            android:theme="@style/PrivacyBrowserAppBar" >
-
-            <!-- The frame layout allows the toolbar and the progress bar to occupy the same space. -->
-            <FrameLayout
-                android:layout_height="wrap_content"
-                android:layout_width="match_parent" >
-
-                <androidx.appcompat.widget.Toolbar
-                    android:id="@+id/view_source_toolbar"
-                    android:layout_height="wrap_content"
-                    android:layout_width="match_parent" />
-
-                <!-- Android automatically uses a different, skinnier drawable with padding for indeterminate horizontal progress bars in API >= 21.
-                    They make this very difficult to override.  https://redmine.stoutner.com/issues/241
-                    `tools:ignore="UnusedAttribute"` removes the lint warning about `progressTint` and `progressBackgroundTint` not applying to API < 21. -->
-                <ProgressBar
-                    android:id="@+id/progress_bar"
-                    style="?android:attr/progressBarStyleHorizontal"
-                    android:layout_height="wrap_content"
-                    android:layout_width="match_parent"
-                    android:minHeight="3dp"
-                    android:layout_gravity="bottom"
-                    android:visibility="gone"
-                    tools:ignore="UnusedAttribute" />
-            </FrameLayout>
-        </com.google.android.material.appbar.AppBarLayout>
-
-        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-            android:id="@+id/view_source_swiperefreshlayout"
-            android:layout_height="match_parent"
-            android:layout_width="match_parent">
-
-            <ScrollView
-                android:id="@+id/view_source_scrollview"
-                android:layout_height="wrap_content"
-                android:layout_width="match_parent" >
-
-                <LinearLayout
-                    android:layout_height="wrap_content"
-                    android:layout_width="match_parent"
-                    android:orientation="vertical"
-                    android:layout_margin="10dp" >
-
-                    <!-- Request headers. -->
-                    <TextView
-                        android:id="@+id/request_headers_title_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:text="@string/request_headers"
-                        android:textAlignment="center"
-                        android:textSize="18sp"
-                        android:textColor="@color/blue_text"
-                        android:textStyle="bold" />
-
-                    <TextView
-                        android:id="@+id/request_headers_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textIsSelectable="true"
-                        android:layout_marginBottom="8dp" />
-
-                    <!-- Response message. -->
-                    <TextView
-                        android:id="@+id/response_message_title_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:text="@string/response_message"
-                        android:textAlignment="center"
-                        android:textSize="18sp"
-                        android:textColor="@color/blue_text"
-                        android:textStyle="bold" />
-
-                    <TextView
-                        android:id="@+id/response_message_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textIsSelectable="true"
-                        android:layout_marginBottom="8dp" />
-
-                    <!-- Response headers. -->
-                    <!-- The title text is set programatically. -->
-                    <TextView
-                        android:id="@+id/response_headers_title_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textAlignment="center"
-                        android:textSize="18sp"
-                        android:textColor="@color/blue_text"
-                        android:textStyle="bold" />
-
-                    <TextView
-                        android:id="@+id/response_headers_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textIsSelectable="true"
-                        android:layout_marginBottom="8dp" />
-
-                    <!-- Response body. -->
-                    <!-- The title text is set programatically. -->
-                    <TextView
-                        android:id="@+id/response_body_title_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textAlignment="center"
-                        android:textSize="18sp"
-                        android:textColor="@color/blue_text"
-                        android:textStyle="bold" />
-
-                    <TextView
-                        android:id="@+id/response_body_textview"
-                        android:layout_height="wrap_content"
-                        android:layout_width="match_parent"
-                        android:textIsSelectable="true" />
-                </LinearLayout>
-            </ScrollView>
-        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
-    </LinearLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/menu/view_headers_options_menu.xml b/app/src/main/res/menu/view_headers_options_menu.xml
new file mode 100644 (file)
index 0000000..d743995
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright 2018,2022-2023 Soren Stoutner <soren@stoutner.com>.
+
+  This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
+
+  Privacy Browser Android is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  Privacy Browser Android is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
+
+<menu
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <!-- `android:iconTint` can be used once API >= 26 instead of including separate drawable files. -->
+    <item
+        android:id="@+id/about_view_headers"
+        android:title="@string/about"
+        android:orderInCategory="10"
+        android:icon="@drawable/about"
+        app:showAsAction="ifRoom" />
+</menu>
diff --git a/app/src/main/res/menu/view_source_options_menu.xml b/app/src/main/res/menu/view_source_options_menu.xml
deleted file mode 100644 (file)
index 0843e1f..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  Copyright © 2018,2022 Soren Stoutner <soren@stoutner.com>.
-
-  This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
-
-  Privacy Browser Android is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License as published by
-  the Free Software Foundation, either version 3 of the License, or
-  (at your option) any later version.
-
-  Privacy Browser Android is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>. -->
-
-<menu
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <!-- `android:iconTint` can be used once API >= 26 instead of including separate drawable files. -->
-    <item
-        android:id="@+id/about_view_source"
-        android:title="@string/about"
-        android:orderInCategory="10"
-        android:icon="@drawable/about"
-        app:showAsAction="ifRoom" />
-</menu>
\ No newline at end of file
index 3e89db026360f4be4f8fda917b118a6a35d9b993..7ec11f85083974f2267649f8a3b217eeec2d9b38 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2015-2023 Soren Stoutner <soren@stoutner.com>.
+  Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
 
                 android:title="@string/view_source"
                 android:orderInCategory="1220"
                 app:showAsAction="never" />
+
+            <item
+                android:id="@+id/view_headers"
+                android:title="@string/view_headers"
+                android:orderInCategory="1230"
+                app:showAsAction="never" />
         </menu>
     </item>
 
         android:title="@string/add_domain_settings"
         android:orderInCategory="1400"
         app:showAsAction="never" />
-</menu>
\ No newline at end of file
+</menu>
index 70e2a78504875ed8e24ffb562d60dd486b4ea66a..ae22a09199a85f9534106decd6d4a3069eaf1ec7 100644 (file)
     <string name="content_data">Content-Daten</string>
     <string name="untrusted_ssl_certificate">Das SSL-Zertifikat ist nicht vertrauenswürdig.</string>
     <string name="load_anyway">Trotzdem laden</string>
-    <string name="about_view_source">Über Quelltext</string>
-    <string name="about_view_source_message">Weil Androids WebView keine Quelltext-Informationen zur Verfügung stellt, muss eine separate a separate Serveranfrage mit system tools gestellt werden,
+    <string name="about_view_headers_message">Weil Androids WebView keine Quelltext-Informationen zur Verfügung stellt, muss eine separate a separate Serveranfrage mit system tools gestellt werden,
         die hier dargestellten Daten erhält. Deshalb können Unterschiede zwischen diesen Daten und der mit WebView dargestellten Webseite auftreten.
         Es ist geplant, mit Version 4.x eine eigene Engine einzuführen, die diese Einschränkungen umgeht.</string>
 
index be8b835b1453dbf03a9c45466ed7fb10205d5958..bf7384abe5d96dc7a092518a8e0a2be7c0c3787c 100644 (file)
     <string name="content_data">Datos del contenido</string>
     <string name="untrusted_ssl_certificate">El certificado SSL no es de confianza.</string>
     <string name="load_anyway">Cargar de todos modos</string>
-    <string name="about_view_source">Acerca de ver la fuente</string>
-    <string name="about_view_source_message">Debido a que WebView de Android no expone la información fuente,
+    <string name="about_view_headers_message">Debido a que WebView de Android no expone la información fuente,
         se hizo una solicitud por separado utilizando las herramientas del sistema para recopilar la información mostrada en esta actividad.
         Puede haber algunas diferencias entre estos datos y los utilizados por WebView en la actividad principal.
         Esta limitación se eliminará en la serie 4.x con el lanzamiento de Privacy WebView.</string>
index 9ca8d80fa45daeea369a1d6d3963dd0b73197eb9..fb0af5a9821f1c16b8caf3675033299c12a45859 100644 (file)
     <string name="content_data">Données du contenu</string>
     <string name="untrusted_ssl_certificate">Le certificat SSL n\'est pas fiable.</string>
     <string name="load_anyway">Charger quand même</string>
-    <string name="about_view_source">A propos de Voir Source</string>
-    <string name="about_view_source_message">Puisqu\'Android Webview ne permet pas de révêler l\'information source,
+    <string name="about_view_headers_message">Puisqu\'Android Webview ne permet pas de révêler l\'information source,
         une requête séparée a été effectuée en utilisant les outils sytèmes afin d\'afficher ce qui est présenté à l\'écran
         Il peut donc y avoir des différences entre ces connées et celle utilisée par Webview. Cette limitation sera supprimée lors de la sortie de Privacy Webview 4.x</string>
 
index 80f7076a2d54c8bc978ba2caa9e2e0464f7bd0b2..de3d784faebf66bfaac8a1270a1aee89cff81348 100644 (file)
     <string name="content_data">Content - Dati</string>
     <string name="untrusted_ssl_certificate">Il certificato SSL non è attendibile.</string>
     <string name="load_anyway">Carica comunque</string>
-    <string name="about_view_source">Informazioni sulla visualizzazione della sorgente</string>
-    <string name="about_view_source_message">Dal momento che la WebView di Android non fornisce indicazioni sulla sorgente è stata effettuata una richiesta separata utilizzando i system tools in modo da
+    <string name="about_view_headers_message">Dal momento che la WebView di Android non fornisce indicazioni sulla sorgente è stata effettuata una richiesta separata utilizzando i system tools in modo da
         ottenere le informazioni mostrate. Potrebbero esserci alcune differenze tra questi dati e quelli utilizzati da WebView.
         Questa limitazione sarà eliminata nella serie 4.x quando verrà rilasciata Privacy WebView.</string>
 
index 2ddd613fea9ab90c062a9f7295172a5995421421..c1b173ff5e55cda47f1d578b2e3945e02f383a57 100644 (file)
     <string name="content_data">Dados de conteúdo</string>
     <string name="untrusted_ssl_certificate">O certificado SSL é suspeito.</string>
     <string name="load_anyway">Carregar mesmo assim</string>
-    <string name="about_view_source">Sobre Ver Fonte</string>
-    <string name="about_view_source_message">Como o WebView do Android não expõe as informações de origem,
+    <string name="about_view_headers_message">Como o WebView do Android não expõe as informações de origem,
         uma solicitação separada foi feita usando ferramentas do sistema para reunir as informações exibidas nesta atividade.
         Pode haver algumas diferenças entre esses dados e aqueles usados pelo WebView na atividade principal. Essa limitação será removida na série 4.x com o lançamento do Privacy WebView.</string>
 
index a755524772894f5c0668aed8c04c6cb3ecb554e4..6f6f2d26ecec3c1030a073d206287317423a1050 100644 (file)
     <string name="content_data">Данные содержимого</string>
     <string name="untrusted_ssl_certificate">SSL-сертификат не является доверенным.</string>
     <string name="load_anyway">Все равно загрузить</string>
-    <string name="about_view_source">О просмотре исходного кода</string>
-    <string name="about_view_source_message">Поскольку Android WebView не предоставляет исходные данные, для сбора информации, отображаемой в этом действии,
+    <string name="about_view_headers_message">Поскольку Android WebView не предоставляет исходные данные, для сбора информации, отображаемой в этом действии,
         был сделан отдельный запрос с помощью системных средств. Между этими данными и теми, которые используются в WebView, могут быть некоторые отличия.
         Это ограничение будет удалено в серии 4.x с выпуском Privacy WebView.</string>
 
index 409afe7224b55292e8d4c0cacdc8bfa44710bd54..83b76eb4efed097d6e310332cda1ed06c7179ac7 100644 (file)
     <string name="response_message">Yanıt Mesajı</string>
     <string name="response_headers">Yanıt Başlıkları</string>
     <string name="response_body">Yanıt Metni</string>
-    <string name="about_view_source">Kaynağı Görüntüle Hakkında</string>
-    <string name="about_view_source_message">Android WebView kaynak bilgisini gösteremediğinden, bu etkinlikte gösterilen bilgiyi toplamak için sistem araçları kullanılarak ayrı bir istek yapıldı.
+    <string name="about_view_headers_message">Android WebView kaynak bilgisini gösteremediğinden, bu etkinlikte gösterilen bilgiyi toplamak için sistem araçları kullanılarak ayrı bir istek yapıldı.
         Elde edilen veri ile ana etkinlikteki WebView\'ın kullandığı veri arasında farklılıklar olabilir. Bu sorun, 4.x serisinde Privacy WebView sürümüyle ortadan kalkacaktır.</string>
 
     <!-- Create Home Screen Shortcut Alert Dialog. -->
index e2a9e08daccfac0059ba04c6463df4bfe2f97c09..376e7c363e0d730bbb788377f928371493e2764b 100644 (file)
     <string name="content_data">内容数据</string>
     <string name="untrusted_ssl_certificate">SSL证书不受信任.</string>
     <string name="load_anyway">仍然加载</string>
-    <string name="about_view_source">查看源代码</string>
-    <string name="about_view_source_message">因为安卓的网页不支持显示资源内容,一个另外的请求被系统调用来显示这个活动的信息,数据和网页主要活动可能存在差异,这个限制将会在隐私浏览器4.x系列解决。</string>
+    <string name="about_view_headers_message">因为安卓的网页不支持显示资源内容,一个另外的请求被系统调用来显示这个活动的信息,数据和网页主要活动可能存在差异,这个限制将会在隐私浏览器4.x系列解决。</string>
 
     <!-- Create Home Screen Shortcut Alert Dialog. -->
     <string name="create_shortcut">创建标签</string>
index bc89d4e3f48c972af885802900d2982b78ff9391..8e57ec0dae08d2e2a62d180205e0d72dd947f8d7 100644 (file)
         <string name="save">Save</string>
         <string name="add_to_home_screen">Add to Home Screen</string>
         <string name="view_source">View Source</string>
+        <string name="view_rendered_website">View Rendered Website</string>
+        <string name="view_headers">View Headers</string>
     <string name="share">Share</string>
         <string name="share_message">Share Message</string>
         <string name="share_url">Share URL</string>
     <string name="content_data">Content Data</string>
     <string name="untrusted_ssl_certificate">The SSL certificate is untrusted.</string>
     <string name="load_anyway">Load anyway</string>
-    <string name="about_view_source">About View Source</string>
-    <string name="about_view_source_message">Because Android’s WebView does not expose the source information,
+    <string name="about_view_headers">About View Headers</string>
+    <string name="about_view_headers_message">Because Android’s WebView does not expose the source information,
         a separate request was made using system tools to gather the information displayed in this activity.
         There may be some differences between this data and that used by the WebView in the main activity. This limitation will be removed in the 4.x series with the release of Privacy WebView.</string>