2 * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
6 * Privacy Browser Android is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser Android is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.activities
22 import android.os.Bundle
23 import android.text.SpannableStringBuilder
24 import android.text.style.ForegroundColorSpan
25 import android.util.TypedValue
26 import android.view.KeyEvent
27 import android.view.Menu
28 import android.view.MenuItem
29 import android.view.View
30 import android.view.View.OnFocusChangeListener
31 import android.view.WindowManager
32 import android.view.inputmethod.InputMethodManager
33 import android.widget.Button
34 import android.widget.EditText
35 import android.widget.ProgressBar
36 import android.widget.TextView
38 import androidx.appcompat.app.ActionBar
39 import androidx.appcompat.app.AppCompatActivity
40 import androidx.appcompat.widget.Toolbar
41 import androidx.core.app.NavUtils
42 import androidx.fragment.app.DialogFragment
43 import androidx.lifecycle.ViewModelProvider
44 import androidx.preference.PreferenceManager
45 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
47 import com.google.android.material.snackbar.Snackbar
49 import com.stoutner.privacybrowser.R
50 import com.stoutner.privacybrowser.dialogs.AVAILABLE_CIPHERS
51 import com.stoutner.privacybrowser.dialogs.SSL_CERTIFICATE
52 import com.stoutner.privacybrowser.dialogs.AboutViewHeadersDialog
53 import com.stoutner.privacybrowser.dialogs.ViewHeadersDetailDialog
54 import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog
55 import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog.UntrustedSslCertificateListener
56 import com.stoutner.privacybrowser.helpers.ProxyHelper
57 import com.stoutner.privacybrowser.helpers.UrlHelper
58 import com.stoutner.privacybrowser.viewmodelfactories.ViewHeadersFactory
59 import com.stoutner.privacybrowser.viewmodels.HeadersViewModel
61 // Define the public constants.
62 const val USER_AGENT = "user_agent"
64 class ViewHeadersActivity: AppCompatActivity(), UntrustedSslCertificateListener {
65 // Declare the class variables.
66 private lateinit var appliedCipherString: String
67 private lateinit var availableCiphersString: String
68 private lateinit var headersViewModel: HeadersViewModel
69 private lateinit var initialGrayColorSpan: ForegroundColorSpan
70 private lateinit var finalGrayColorSpan: ForegroundColorSpan
71 private lateinit var redColorSpan: ForegroundColorSpan
72 private lateinit var sslCertificateString: String
74 // Declare the class views.
75 private lateinit var urlEditText: EditText
76 private lateinit var sslInformationTitleTextView: TextView
77 private lateinit var sslInformationTextView: TextView
78 private lateinit var ciphersButton: Button
79 private lateinit var certificateButton: Button
80 private lateinit var requestHeadersTitleTextView: TextView
81 private lateinit var requestHeadersTextView: TextView
82 private lateinit var responseMessageTitleTextView: TextView
83 private lateinit var responseMessageTextView: TextView
84 private lateinit var responseHeadersTitleTextView: TextView
85 private lateinit var responseBodyTitleTextView: TextView
87 override fun onCreate(savedInstanceState: Bundle?) {
88 // Get a handle for the shared preferences.
89 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
91 // Get the preferences.
92 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
93 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
95 // Disable screenshots if not allowed.
96 if (!allowScreenshots) {
97 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
100 // Run the default commands.
101 super.onCreate(savedInstanceState)
103 // Get the launching intent
106 // Get the information from the intent.
107 val currentUrl = intent.getStringExtra(CURRENT_URL)!!
108 val userAgent = intent.getStringExtra(USER_AGENT)!!
110 // Set the content view.
112 setContentView(R.layout.view_headers_bottom_appbar)
114 setContentView(R.layout.view_headers_top_appbar)
117 // Get a handle for the toolbar.
118 val toolbar = findViewById<Toolbar>(R.id.toolbar)
120 // Set the support action bar.
121 setSupportActionBar(toolbar)
123 // Get a handle for the action bar.
124 val actionBar = supportActionBar!!
126 // Add the custom layout to the action bar.
127 actionBar.setCustomView(R.layout.view_headers_appbar_custom_view)
129 // Instruct the action bar to display a custom layout.
130 actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
132 // Get handles for the views.
133 urlEditText = findViewById(R.id.url_edittext)
134 val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
135 val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.swiperefreshlayout)
136 sslInformationTitleTextView = findViewById(R.id.ssl_information_title_textview)
137 sslInformationTextView = findViewById(R.id.ssl_information_textview)
138 ciphersButton = findViewById(R.id.ciphers_button)
139 certificateButton = findViewById(R.id.certificate_button)
140 requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
141 requestHeadersTextView = findViewById(R.id.request_headers_textview)
142 responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
143 responseMessageTextView = findViewById(R.id.response_message_textview)
144 responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textview)
145 val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
146 responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
147 val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
149 // Populate the URL text box.
150 urlEditText.setText(currentUrl)
152 // Initialize the gray foreground color spans for highlighting the URLs.
153 initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
154 finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
155 redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
157 // Apply text highlighting to the URL.
158 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
160 // Get a handle for the input method manager, which is used to hide the keyboard.
161 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
163 // Remove the formatting from the URL when the user is editing the text.
164 urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
165 if (hasFocus) { // The user is editing the URL text box.
166 // Remove the highlighting.
167 urlEditText.text.removeSpan(redColorSpan)
168 urlEditText.text.removeSpan(initialGrayColorSpan)
169 urlEditText.text.removeSpan(finalGrayColorSpan)
170 } else { // The user has stopped editing the URL text box.
171 // Hide the soft keyboard.
172 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
174 // Move to the beginning of the string.
175 urlEditText.setSelection(0)
177 // Reapply the highlighting.
178 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
182 // Set the refresh color scheme according to the theme.
183 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
185 // Initialize a color background typed value.
186 val colorBackgroundTypedValue = TypedValue()
188 // Get the color background from the theme.
189 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
191 // Get the color background int from the typed value.
192 val colorBackgroundInt = colorBackgroundTypedValue.data
194 // Set the swipe refresh background color.
195 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
197 // Get the list of locales.
198 val localeList = resources.configuration.locales
200 // Initialize a string builder to extract the locales from the list.
201 val localesStringBuilder = StringBuilder()
203 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
206 // Populate the string builder with the contents of the locales list.
207 for (i in 0 until localeList.size()) {
208 // Append a comma if there is already an item in the string builder.
210 localesStringBuilder.append(",")
213 // Get the locale from the list.
214 val locale = localeList[i]
216 // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
217 localesStringBuilder.append(locale.language)
218 localesStringBuilder.append("-")
219 localesStringBuilder.append(locale.country)
221 // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
223 localesStringBuilder.append(";q=0.")
224 localesStringBuilder.append(q)
227 // Decrement `q` if it is greater than 1.
232 // Add a second entry for the language only portion of the locale.
233 localesStringBuilder.append(",")
234 localesStringBuilder.append(locale.language)
236 // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
237 localesStringBuilder.append(";q=0.")
238 localesStringBuilder.append(q)
240 // Decrement `q` if it is greater than 1.
246 // Instantiate the proxy helper.
247 val proxyHelper = ProxyHelper()
249 // Get the current proxy.
250 val proxy = proxyHelper.getCurrentProxy(this)
252 // Make the progress bar visible.
253 progressBar.visibility = View.VISIBLE
255 // Set the progress bar to be indeterminate.
256 progressBar.isIndeterminate = true
258 // Update the layout.
259 updateLayout(currentUrl)
261 // Instantiate the view headers factory.
262 val viewHeadersFactory: ViewModelProvider.Factory = ViewHeadersFactory(application, currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
264 // Instantiate the headers view model.
265 headersViewModel = ViewModelProvider(this, viewHeadersFactory)[HeadersViewModel::class.java]
267 // Create a headers observer.
268 headersViewModel.observeHeaders().observe(this) { headersStringArray: Array<SpannableStringBuilder> ->
269 // Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large.
270 sslInformationTextView.text = headersStringArray[0]
271 requestHeadersTextView.text = headersStringArray[4]
272 responseMessageTextView.text = headersStringArray[5]
273 responseHeadersTextView.text = headersStringArray[6]
274 responseBodyTextView.text = headersStringArray[7]
276 // Populate the dialog strings.
277 appliedCipherString = headersStringArray[1].toString()
278 availableCiphersString = headersStringArray[2].toString()
279 sslCertificateString = headersStringArray[3].toString()
281 // Hide the progress bar.
282 progressBar.isIndeterminate = false
283 progressBar.visibility = View.GONE
285 // Stop the swipe to refresh indicator if it is running
286 swipeRefreshLayout.isRefreshing = false
289 // Create an error observer.
290 headersViewModel.observeErrors().observe(this) { errorString: String ->
291 // Display an error snackbar if the string is not `""`.
292 if (errorString != "") {
293 if (errorString.startsWith("javax.net.ssl.SSLHandshakeException")) {
294 // Instantiate the untrusted SSL certificate dialog.
295 val untrustedSslCertificateDialog = UntrustedSslCertificateDialog()
297 // Show the untrusted SSL certificate dialog.
298 untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
300 // Display a snackbar with the error message.
301 Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
306 // Implement swipe to refresh.
307 swipeRefreshLayout.setOnRefreshListener {
308 // Make the progress bar visible.
309 progressBar.visibility = View.VISIBLE
311 // Set the progress bar to be indeterminate.
312 progressBar.isIndeterminate = true
315 val urlString = urlEditText.text.toString()
317 // Update the layout.
318 updateLayout(urlString)
320 // Get the updated headers.
321 headersViewModel.updateHeaders(urlString, false)
324 // Set the go button on the keyboard to request new headers data.
325 urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
326 // Request new headers data if the enter key was pressed.
327 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
328 // Hide the soft keyboard.
329 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
331 // Remove the focus from the URL box.
332 urlEditText.clearFocus()
334 // Make the progress bar visible.
335 progressBar.visibility = View.VISIBLE
337 // Set the progress bar to be indeterminate.
338 progressBar.isIndeterminate = true
341 val urlString = urlEditText.text.toString()
343 // Update the layout.
344 updateLayout(urlString)
346 // Get the updated headers.
347 headersViewModel.updateHeaders(urlString, false)
349 // Consume the key press.
350 return@setOnKeyListener true
352 // Do not consume the key press.
353 return@setOnKeyListener false
358 override fun onCreateOptionsMenu(menu: Menu): Boolean {
360 menuInflater.inflate(R.menu.view_headers_options_menu, menu)
366 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
367 // Instantiate the about dialog fragment.
368 val aboutDialogFragment: DialogFragment = AboutViewHeadersDialog()
370 // Show the about alert dialog.
371 aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
373 // Consume the event.
377 // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
378 fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
380 NavUtils.navigateUpFromSameTask(this)
383 override fun loadAnyway() {
384 // Load the URL anyway.
385 headersViewModel.updateHeaders(urlEditText.text.toString(), true)
388 // The view parameter cannot be removed because it is called from the layout onClick.
389 fun showCertificate(@Suppress("UNUSED_PARAMETER")view: View) {
390 // Instantiate an SSL certificate dialog.
391 val sslCertificateDialogFragment= ViewHeadersDetailDialog.displayDialog(SSL_CERTIFICATE, sslCertificateString)
394 sslCertificateDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
397 // The view parameter cannot be removed because it is called from the layout onClick.
398 fun showCiphers(@Suppress("UNUSED_PARAMETER")view: View) {
399 // Instantiate an SSL certificate dialog.
400 val ciphersDialogFragment= ViewHeadersDetailDialog.displayDialog(AVAILABLE_CIPHERS, availableCiphersString, appliedCipherString)
403 ciphersDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
406 private fun updateLayout(urlString: String) {
407 if (urlString.startsWith("content://")) { // This is a content URL.
408 // Hide the unused views.
409 sslInformationTitleTextView.visibility = View.GONE
410 sslInformationTextView.visibility = View.GONE
411 ciphersButton.visibility = View.GONE
412 certificateButton.visibility = View.GONE
413 requestHeadersTitleTextView.visibility = View.GONE
414 requestHeadersTextView.visibility = View.GONE
415 responseMessageTitleTextView.visibility = View.GONE
416 responseMessageTextView.visibility = View.GONE
418 // Change the text of the remaining title text views.
419 responseHeadersTitleTextView.setText(R.string.content_metadata)
420 responseBodyTitleTextView.setText(R.string.content_data)
421 } else { // This is not a content URL.
422 // Set the status if the the SSL information views.
423 if (urlString.startsWith("http://")) { // This is an HTTP URL.
424 // Hide the SSL information views.
425 sslInformationTitleTextView.visibility = View.GONE
426 sslInformationTextView.visibility = View.GONE
427 ciphersButton.visibility = View.GONE
428 certificateButton.visibility = View.GONE
429 } else { // This is not an HTTP URL.
430 // Show the SSL information views.
431 sslInformationTitleTextView.visibility = View.VISIBLE
432 sslInformationTextView.visibility = View.VISIBLE
433 ciphersButton.visibility = View.VISIBLE
434 certificateButton.visibility = View.VISIBLE
437 // Show the other views.
438 requestHeadersTitleTextView.visibility = View.VISIBLE
439 requestHeadersTextView.visibility = View.VISIBLE
440 responseMessageTitleTextView.visibility = View.VISIBLE
441 responseMessageTextView.visibility = View.VISIBLE
443 // Restore the text of the other title text views.
444 responseHeadersTitleTextView.setText(R.string.response_headers)
445 responseBodyTitleTextView.setText(R.string.response_body)