/* * Copyright 2017-2023 Soren Stoutner . * * This file is part of Privacy Browser Android . * * Privacy Browser Android is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Privacy Browser Android is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Privacy Browser Android. If not, see . */ package com.stoutner.privacybrowser.activities import android.os.Bundle import android.text.SpannableStringBuilder import android.text.style.ForegroundColorSpan import android.util.TypedValue import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.View import android.view.View.OnFocusChangeListener import android.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.Button 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.AVAILABLE_CIPHERS import com.stoutner.privacybrowser.dialogs.SSL_CERTIFICATE import com.stoutner.privacybrowser.dialogs.AboutViewHeadersDialog import com.stoutner.privacybrowser.dialogs.ViewHeadersDetailDialog 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 appliedCipherString: String private lateinit var availableCiphersString: String private lateinit var headersViewModel: HeadersViewModel private lateinit var initialGrayColorSpan: ForegroundColorSpan private lateinit var finalGrayColorSpan: ForegroundColorSpan private lateinit var redColorSpan: ForegroundColorSpan private lateinit var sslCertificateString: String // Declare the class views. private lateinit var urlEditText: EditText private lateinit var sslInformationTitleTextView: TextView private lateinit var sslInformationTextView: TextView private lateinit var ciphersButton: Button private lateinit var certificateButton: Button private lateinit var requestHeadersTitleTextView: TextView private lateinit var requestHeadersTextView: TextView private lateinit var responseMessageTitleTextView: TextView private lateinit var responseMessageTextView: TextView private lateinit var responseHeadersTitleTextView: TextView private lateinit var responseBodyTitleTextView: TextView override fun onCreate(savedInstanceState: Bundle?) { // Get a handle for the shared preferences. val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) // Get the preferences. val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false) val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false) // Disable screenshots if not allowed. if (!allowScreenshots) { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) } // Run the default commands. super.onCreate(savedInstanceState) // Get the launching intent val intent = intent // Get the information from the intent. val currentUrl = intent.getStringExtra(CURRENT_URL)!! val userAgent = intent.getStringExtra(USER_AGENT)!! // Set the content view. if (bottomAppBar) { setContentView(R.layout.view_headers_bottom_appbar) } else { setContentView(R.layout.view_headers_top_appbar) } // Get a handle for the toolbar. val toolbar = findViewById(R.id.toolbar) // Set the support action bar. setSupportActionBar(toolbar) // Get a handle for the action bar. val actionBar = supportActionBar!! // Add the custom layout to the action bar. actionBar.setCustomView(R.layout.view_headers_appbar_custom_view) // Instruct the action bar to display a custom layout. actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM // Get handles for the views. urlEditText = findViewById(R.id.url_edittext) val progressBar = findViewById(R.id.progress_bar) val swipeRefreshLayout = findViewById(R.id.swiperefreshlayout) sslInformationTitleTextView = findViewById(R.id.ssl_information_title_textview) sslInformationTextView = findViewById(R.id.ssl_information_textview) ciphersButton = findViewById(R.id.ciphers_button) certificateButton = findViewById(R.id.certificate_button) requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview) requestHeadersTextView = findViewById(R.id.request_headers_textview) responseMessageTitleTextView = findViewById(R.id.response_message_title_textview) responseMessageTextView = findViewById(R.id.response_message_textview) responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textview) val responseHeadersTextView = findViewById(R.id.response_headers_textview) responseBodyTitleTextView = findViewById(R.id.response_body_title_textview) val responseBodyTextView = findViewById(R.id.response_body_textview) // Populate the URL text box. urlEditText.setText(currentUrl) // Initialize the gray foreground color spans for highlighting the URLs. initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500)) finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500)) redColorSpan = ForegroundColorSpan(getColor(R.color.red_text)) // Apply text highlighting to the URL. UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan) // Get a handle for the input method manager, which is used to hide the keyboard. val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager) // Remove the formatting from the URL when the user is editing the text. urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean -> if (hasFocus) { // The user is editing the URL text box. // Remove the highlighting. urlEditText.text.removeSpan(redColorSpan) urlEditText.text.removeSpan(initialGrayColorSpan) urlEditText.text.removeSpan(finalGrayColorSpan) } else { // The user has stopped editing the URL text box. // Hide the soft keyboard. inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0) // Move to the beginning of the string. urlEditText.setSelection(0) // Reapply the highlighting. UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan) } } // Set the refresh color scheme according to the theme. swipeRefreshLayout.setColorSchemeResources(R.color.blue_text) // Initialize a color background typed value. val colorBackgroundTypedValue = TypedValue() // Get the color background from the theme. theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true) // Get the color background int from the typed value. val colorBackgroundInt = colorBackgroundTypedValue.data // Set the swipe refresh background color. swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt) // Get the list of locales. val localeList = resources.configuration.locales // Initialize a string builder to extract the locales from the list. val localesStringBuilder = StringBuilder() // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages. var q = 10 // Populate the string builder with the contents of the locales list. for (i in 0 until localeList.size()) { // Append a comma if there is already an item in the string builder. if (i > 0) { localesStringBuilder.append(",") } // Get the locale from the list. val locale = localeList[i] // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format. localesStringBuilder.append(locale.language) localesStringBuilder.append("-") localesStringBuilder.append(locale.country) // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1. if (q < 10) { localesStringBuilder.append(";q=0.") localesStringBuilder.append(q) } // Decrement `q` if it is greater than 1. if (q > 1) { q-- } // Add a second entry for the language only portion of the locale. localesStringBuilder.append(",") localesStringBuilder.append(locale.language) // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1. localesStringBuilder.append(";q=0.") localesStringBuilder.append(q) // Decrement `q` if it is greater than 1. if (q > 1) { q-- } } // Instantiate the proxy helper. val proxyHelper = ProxyHelper() // Get the current proxy. val proxy = proxyHelper.getCurrentProxy(this) // Make the progress bar visible. progressBar.visibility = View.VISIBLE // Set the progress bar to be indeterminate. progressBar.isIndeterminate = true // Update the layout. updateLayout(currentUrl) // Instantiate the view headers factory. val viewHeadersFactory: ViewModelProvider.Factory = ViewHeadersFactory(application, currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService) // Instantiate the headers view model. headersViewModel = ViewModelProvider(this, viewHeadersFactory)[HeadersViewModel::class.java] // Create a headers observer. headersViewModel.observeHeaders().observe(this) { headersStringArray: Array -> // Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large. sslInformationTextView.text = headersStringArray[0] requestHeadersTextView.text = headersStringArray[4] responseMessageTextView.text = headersStringArray[5] responseHeadersTextView.text = headersStringArray[6] responseBodyTextView.text = headersStringArray[7] // Populate the dialog strings. appliedCipherString = headersStringArray[1].toString() availableCiphersString = headersStringArray[2].toString() sslCertificateString = headersStringArray[3].toString() // 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) } // The view parameter cannot be removed because it is called from the layout onClick. fun showCertificate(@Suppress("UNUSED_PARAMETER")view: View) { // Instantiate an SSL certificate dialog. val sslCertificateDialogFragment= ViewHeadersDetailDialog.displayDialog(SSL_CERTIFICATE, sslCertificateString) // Show the dialog. sslCertificateDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate)) } // The view parameter cannot be removed because it is called from the layout onClick. fun showCiphers(@Suppress("UNUSED_PARAMETER")view: View) { // Instantiate an SSL certificate dialog. val ciphersDialogFragment= ViewHeadersDetailDialog.displayDialog(AVAILABLE_CIPHERS, availableCiphersString, appliedCipherString) // Show the dialog. ciphersDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate)) } private fun updateLayout(urlString: String) { if (urlString.startsWith("content://")) { // This is a content URL. // Hide the unused views. sslInformationTitleTextView.visibility = View.GONE sslInformationTextView.visibility = View.GONE ciphersButton.visibility = View.GONE certificateButton.visibility = View.GONE 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. // Set the status if the the SSL information views. if (urlString.startsWith("http://")) { // This is an HTTP URL. // Hide the SSL information views. sslInformationTitleTextView.visibility = View.GONE sslInformationTextView.visibility = View.GONE ciphersButton.visibility = View.GONE certificateButton.visibility = View.GONE } else { // This is not an HTTP URL. // Show the SSL information views. sslInformationTitleTextView.visibility = View.VISIBLE sslInformationTextView.visibility = View.VISIBLE ciphersButton.visibility = View.VISIBLE certificateButton.visibility = View.VISIBLE } // Show the other 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) } } }