From: Soren Stoutner Date: Wed, 21 Jun 2023 21:43:17 +0000 (-0700) Subject: Use the secret built-in View Source. https://redmine.stoutner.com/issues/1023 X-Git-Tag: v3.15.1~3 X-Git-Url: https://gitweb.stoutner.com/?p=PrivacyBrowserAndroid.git;a=commitdiff_plain;h=a54a66c7d169d2edf55ba560ec2d951e709188e6 Use the secret built-in View Source. https://redmine.stoutner.com/issues/1023 --- diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4cfa09bd..968bfeb1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ { // 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 index 00000000..2f0aeb0b --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt @@ -0,0 +1,390 @@ +/* + * Copyright 2017-2023 Soren Stoutner . + * + * This file is part of 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 . + */ + +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(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(R.id.progress_bar) + val swipeRefreshLayout = findViewById(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(R.id.response_headers_textview) + responseBodyTitleTextView = findViewById(R.id.response_body_title_textview) + val responseBodyTextView = findViewById(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 -> + // 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 index 736f67d3..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright 2017-2023 Soren Stoutner . - * - * This file is part of 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 . - */ - -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(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(R.id.progress_bar) - val swipeRefreshLayout = findViewById(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(R.id.response_headers_textview) - responseBodyTitleTextView = findViewById(R.id.response_body_title_textview) - val responseBodyTextView = findViewById(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 -> - // 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 index 00000000..030ce4c6 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetHeadersBackgroundTask.kt @@ -0,0 +1,336 @@ +/* + * Copyright 2017-2023 Soren Stoutner . + * + * This file is part of 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 . + */ + +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 { + + // 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( + object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted(chain: Array, authType: String) { + // Do nothing, which trusts all client certificates. + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted(chain: Array, authType: String) { + // Do nothing, which trusts all server certificates. + } + + override fun getAcceptedIssuers(): Array? { + return null + } + } + ) + + // Get an SSL context. `TLS` provides a base instance available from API 1. + 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 index f7ad0a5b..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/backgroundtasks/GetSourceBackgroundTask.kt +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright © 2017-2023 Soren Stoutner . - * - * This file is part of 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 . - */ - -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 { - - // 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( - object : X509TrustManager { - @SuppressLint("TrustAllX509TrustManager") - override fun checkClientTrusted(chain: Array, authType: String) { - // Do nothing, which trusts all client certificates. - } - - @SuppressLint("TrustAllX509TrustManager") - override fun checkServerTrusted(chain: Array, authType: String) { - // Do nothing, which trusts all server certificates. - } - - override fun getAcceptedIssuers(): Array? { - return null - } - } - ) - - // Get an SSL context. `TLS` provides a base instance available from API 1. - 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 index 00000000..f88da7b0 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/AboutViewHeadersDialog.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2018-2023 Soren Stoutner . + * + * This file is part of 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 . + */ + +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 index 8ac83313..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/dialogs/AboutViewSourceDialog.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright © 2018-2022 Soren Stoutner . - * - * This file is part of 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 . - */ - -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 index 00000000..83f0a365 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/ViewHeadersFactory.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2023 Soren Stoutner . + * + * This file is part of 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 . + */ + +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 create(modelClass: Class): 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 index f8de88fd..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/viewmodelfactories/WebViewSourceFactory.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright © 2020-2022 Soren Stoutner . - * - * This file is part of 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 . - */ - -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 create(modelClass: Class): 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 index 00000000..ccfe79ab --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/viewmodels/HeadersViewModel.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2020-2023 Soren Stoutner . + * + * This file is part of 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 . + */ + +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>() + private val mutableLiveDataErrorString = MutableLiveData() + + // 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> { + // Return the source to the activity. + return mutableLiveDataSourceStringArray + } + + // The error observer. + fun observeErrors(): LiveData { + // 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 index d55540ec..00000000 --- a/app/src/main/java/com/stoutner/privacybrowser/viewmodels/WebViewSource.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright © 2020-2022 Soren Stoutner . - * - * This file is part of 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 . - */ - -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>() - private val mutableLiveDataErrorString = MutableLiveData() - - // 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> { - // Return the source to the activity. - return mutableLiveDataSourceStringArray - } - - // The error observer. - fun observeErrors(): LiveData { - // 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 index 00000000..17982c08 --- /dev/null +++ b/app/src/main/res/layout/view_headers_appbar_custom_view.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + 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 index 00000000..0e05cafd --- /dev/null +++ b/app/src/main/res/layout/view_headers_bottom_appbar.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 index 00000000..3171bbcb --- /dev/null +++ b/app/src/main/res/layout/view_headers_top_appbar.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 index 83a465c8..00000000 --- a/app/src/main/res/layout/view_source_appbar_custom_view.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - \ 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 index 852d1c0c..00000000 --- a/app/src/main/res/layout/view_source_bottom_appbar.xml +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 index a63e9419..00000000 --- a/app/src/main/res/layout/view_source_top_appbar.xml +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 index 00000000..d7439959 --- /dev/null +++ b/app/src/main/res/menu/view_headers_options_menu.xml @@ -0,0 +1,32 @@ + + + + + + + + + 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 index 0843e1f0..00000000 --- a/app/src/main/res/menu/view_source_options_menu.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/webview_options_menu.xml b/app/src/main/res/menu/webview_options_menu.xml index 3e89db02..7ec11f85 100644 --- a/app/src/main/res/menu/webview_options_menu.xml +++ b/app/src/main/res/menu/webview_options_menu.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e2a9e08d..376e7c36 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -222,8 +222,7 @@ 内容数据 SSL证书不受信任. 仍然加载 - 查看源代码 - 因为安卓的网页不支持显示资源内容,一个另外的请求被系统调用来显示这个活动的信息,数据和网页主要活动可能存在差异,这个限制将会在隐私浏览器4.x系列解决。 + 因为安卓的网页不支持显示资源内容,一个另外的请求被系统调用来显示这个活动的信息,数据和网页主要活动可能存在差异,这个限制将会在隐私浏览器4.x系列解决。 创建标签 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc89d4e3..8e57ec0d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -172,6 +172,8 @@ Save Add to Home Screen View Source + View Rendered Website + View Headers Share Share Message Share URL @@ -229,8 +231,8 @@ Content Data The SSL certificate is untrusted. Load anyway - About View Source - Because Android’s WebView does not expose the source information, + About View Headers + 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.