<?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"
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
// 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)
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.
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)
// 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.
}
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
// Load the previous website in the history.
currentWebView!!.goBack()
+
+ // Update the URL edit text after a delay.
+ updateUrlEditTextAfterDelay()
}
}
// Load the next website in the history.
currentWebView!!.goForward()
+
+ // Update the URL edit text after a delay.
+ updateUrlEditTextAfterDelay()
}
}
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)
// 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)
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://") ||
// Load the history entry.
currentWebView!!.goBackOrForward(steps)
+
+ // Update the URL edit text after a delay.
+ updateUrlEditTextAfterDelay()
}
override fun openFile(dialogFragment: DialogFragment) {
// Go back.
currentWebView!!.goBack()
+
+ // Update the URL edit text after a delay.
+ updateUrlEditTextAfterDelay()
}
private fun sanitizeUrl(urlString: String): String {
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)
+ }
}
--- /dev/null
+/*
+ * 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)
+ }
+ }
+}
+++ /dev/null
-/*
- * 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)
- }
- }
-}
--- /dev/null
+/*
+ * 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)
+ }
+}
+++ /dev/null
-/*
- * 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
--- /dev/null
+/*
+ * 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
+ }
+}
+++ /dev/null
-/*
- * 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
--- /dev/null
+/*
+ * 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)
+ }
+}
+++ /dev/null
-/*
- * 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
--- /dev/null
+/*
+ * 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)) }
+ }
+}
+++ /dev/null
-/*
- * 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
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
+++ /dev/null
-<?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
+++ /dev/null
-<?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>
+++ /dev/null
-<?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>
--- /dev/null
+<?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>
+++ /dev/null
-<?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
<?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>
<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>
<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>
<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>
<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>
<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>
<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>
<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. -->
<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>
<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>