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 // Initialize the gray foreground color spans for highlighting the URLs.
150 initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
151 finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
152 redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
154 // Get a handle for the input method manager, which is used to hide the keyboard.
155 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
157 // Remove the formatting from the URL when the user is editing the text.
158 urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
159 if (hasFocus) { // The user is editing the URL text box.
160 // Get the foreground color spans.
161 val foregroundColorSpans: Array<ForegroundColorSpan> = urlEditText.text.getSpans(0, urlEditText.text.length, ForegroundColorSpan::class.java)
163 // Remove each foreground color span that highlights the text.
164 for (foregroundColorSpan in foregroundColorSpans)
165 urlEditText.text.removeSpan(foregroundColorSpan)
166 } else { // The user has stopped editing the URL text box.
167 // Hide the soft keyboard.
168 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
170 // Move to the beginning of the string.
171 urlEditText.setSelection(0)
173 // Store the URL text in the intent, so update layout uses the new text if the app is restarted.
174 intent.putExtra(CURRENT_URL, urlEditText.text.toString())
176 // Reapply the highlighting.
177 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
181 // Populate the URL text box.
182 urlEditText.setText(currentUrl)
184 // Apply the initial text highlighting to the URL.
185 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
187 // Set the refresh color scheme according to the theme.
188 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
190 // Initialize a color background typed value.
191 val colorBackgroundTypedValue = TypedValue()
193 // Get the color background from the theme.
194 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
196 // Get the color background int from the typed value.
197 val colorBackgroundInt = colorBackgroundTypedValue.data
199 // Set the swipe refresh background color.
200 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
202 // Get the list of locales.
203 val localeList = resources.configuration.locales
205 // Initialize a string builder to extract the locales from the list.
206 val localesStringBuilder = StringBuilder()
208 // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
211 // Populate the string builder with the contents of the locales list.
212 for (i in 0 until localeList.size()) {
213 // Append a comma if there is already an item in the string builder.
215 localesStringBuilder.append(",")
218 // Get the locale from the list.
219 val locale = localeList[i]
221 // Add the locale to the string. `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
222 localesStringBuilder.append(locale.language)
223 localesStringBuilder.append("-")
224 localesStringBuilder.append(locale.country)
226 // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
228 localesStringBuilder.append(";q=0.")
229 localesStringBuilder.append(q)
232 // Decrement `q` if it is greater than 1.
237 // Add a second entry for the language only portion of the locale.
238 localesStringBuilder.append(",")
239 localesStringBuilder.append(locale.language)
241 // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
242 localesStringBuilder.append(";q=0.")
243 localesStringBuilder.append(q)
245 // Decrement `q` if it is greater than 1.
251 // Instantiate the proxy helper.
252 val proxyHelper = ProxyHelper()
254 // Get the current proxy.
255 val proxy = proxyHelper.getCurrentProxy(this)
257 // Make the progress bar visible.
258 progressBar.visibility = View.VISIBLE
260 // Set the progress bar to be indeterminate.
261 progressBar.isIndeterminate = true
263 // Update the layout.
264 updateLayout(currentUrl)
266 // Instantiate the view headers factory.
267 val viewHeadersFactory: ViewModelProvider.Factory = ViewHeadersFactory(application, currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
269 // Instantiate the headers view model.
270 headersViewModel = ViewModelProvider(this, viewHeadersFactory)[HeadersViewModel::class.java]
272 // Create a headers observer.
273 headersViewModel.observeHeaders().observe(this) { headersStringArray: Array<SpannableStringBuilder> ->
274 // Populate the text views. This can take a long time, and freezes the user interface, if the response body is particularly large.
275 sslInformationTextView.text = headersStringArray[0]
276 requestHeadersTextView.text = headersStringArray[4]
277 responseMessageTextView.text = headersStringArray[5]
278 responseHeadersTextView.text = headersStringArray[6]
279 responseBodyTextView.text = headersStringArray[7]
281 // Populate the dialog strings.
282 appliedCipherString = headersStringArray[1].toString()
283 availableCiphersString = headersStringArray[2].toString()
284 sslCertificateString = headersStringArray[3].toString()
286 // Hide the progress bar.
287 progressBar.isIndeterminate = false
288 progressBar.visibility = View.GONE
290 // Stop the swipe to refresh indicator if it is running
291 swipeRefreshLayout.isRefreshing = false
294 // Create an error observer.
295 headersViewModel.observeErrors().observe(this) { errorString: String ->
296 // Display an error snackbar if the string is not `""`.
297 if (errorString != "") {
298 if (errorString.startsWith("javax.net.ssl.SSLHandshakeException")) {
299 // Instantiate the untrusted SSL certificate dialog.
300 val untrustedSslCertificateDialog = UntrustedSslCertificateDialog()
302 // Show the untrusted SSL certificate dialog.
303 untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
305 // Display a snackbar with the error message.
306 Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
311 // Implement swipe to refresh.
312 swipeRefreshLayout.setOnRefreshListener {
313 // Make the progress bar visible.
314 progressBar.visibility = View.VISIBLE
316 // Set the progress bar to be indeterminate.
317 progressBar.isIndeterminate = true
320 val urlString = urlEditText.text.toString()
322 // Update the layout.
323 updateLayout(urlString)
325 // Get the updated headers.
326 headersViewModel.updateHeaders(urlString, false)
329 // Set the go button on the keyboard to request new headers data.
330 urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
331 // Request new headers data if the enter key was pressed.
332 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
333 // Hide the soft keyboard.
334 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
336 // Remove the focus from the URL box.
337 urlEditText.clearFocus()
339 // Make the progress bar visible.
340 progressBar.visibility = View.VISIBLE
342 // Set the progress bar to be indeterminate.
343 progressBar.isIndeterminate = true
346 val urlString = urlEditText.text.toString()
348 // Update the layout.
349 updateLayout(urlString)
351 // Get the updated headers.
352 headersViewModel.updateHeaders(urlString, false)
354 // Consume the key press.
355 return@setOnKeyListener true
357 // Do not consume the key press.
358 return@setOnKeyListener false
363 override fun onCreateOptionsMenu(menu: Menu): Boolean {
365 menuInflater.inflate(R.menu.view_headers_options_menu, menu)
371 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
372 // Instantiate the about dialog fragment.
373 val aboutDialogFragment: DialogFragment = AboutViewHeadersDialog()
375 // Show the about alert dialog.
376 aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
378 // Consume the event.
382 // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
383 fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
385 NavUtils.navigateUpFromSameTask(this)
388 override fun loadAnyway() {
389 // Load the URL anyway.
390 headersViewModel.updateHeaders(urlEditText.text.toString(), true)
393 // The view parameter cannot be removed because it is called from the layout onClick.
394 fun showCertificate(@Suppress("UNUSED_PARAMETER")view: View) {
395 // Instantiate an SSL certificate dialog.
396 val sslCertificateDialogFragment= ViewHeadersDetailDialog.displayDialog(SSL_CERTIFICATE, sslCertificateString)
399 sslCertificateDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
402 // The view parameter cannot be removed because it is called from the layout onClick.
403 fun showCiphers(@Suppress("UNUSED_PARAMETER")view: View) {
404 // Instantiate an SSL certificate dialog.
405 val ciphersDialogFragment= ViewHeadersDetailDialog.displayDialog(AVAILABLE_CIPHERS, availableCiphersString, appliedCipherString)
408 ciphersDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
411 private fun updateLayout(urlString: String) {
412 if (urlString.startsWith("content://")) { // This is a content URL.
413 // Hide the unused views.
414 sslInformationTitleTextView.visibility = View.GONE
415 sslInformationTextView.visibility = View.GONE
416 ciphersButton.visibility = View.GONE
417 certificateButton.visibility = View.GONE
418 requestHeadersTitleTextView.visibility = View.GONE
419 requestHeadersTextView.visibility = View.GONE
420 responseMessageTitleTextView.visibility = View.GONE
421 responseMessageTextView.visibility = View.GONE
423 // Change the text of the remaining title text views.
424 responseHeadersTitleTextView.setText(R.string.content_metadata)
425 responseBodyTitleTextView.setText(R.string.content_data)
426 } else { // This is not a content URL.
427 // Set the status if the the SSL information views.
428 if (urlString.startsWith("http://")) { // This is an HTTP URL.
429 // Hide the SSL information views.
430 sslInformationTitleTextView.visibility = View.GONE
431 sslInformationTextView.visibility = View.GONE
432 ciphersButton.visibility = View.GONE
433 certificateButton.visibility = View.GONE
434 } else { // This is not an HTTP URL.
435 // Show the SSL information views.
436 sslInformationTitleTextView.visibility = View.VISIBLE
437 sslInformationTextView.visibility = View.VISIBLE
438 ciphersButton.visibility = View.VISIBLE
439 certificateButton.visibility = View.VISIBLE
442 // Show the other views.
443 requestHeadersTitleTextView.visibility = View.VISIBLE
444 requestHeadersTextView.visibility = View.VISIBLE
445 responseMessageTitleTextView.visibility = View.VISIBLE
446 responseMessageTextView.visibility = View.VISIBLE
448 // Restore the text of the other title text views.
449 responseHeadersTitleTextView.setText(R.string.response_headers)
450 responseBodyTitleTextView.setText(R.string.response_body)