]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt
Display SSL information in View Headers. https://redmine.stoutner.com/issues/706
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ViewHeadersActivity.kt
1 /*
2  * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.activities
21
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
37
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
46
47 import com.google.android.material.snackbar.Snackbar
48
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
60
61 // Define the public constants.
62 const val USER_AGENT = "user_agent"
63
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
73
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
86
87     override fun onCreate(savedInstanceState: Bundle?) {
88         // Get a handle for the shared preferences.
89         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
90
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)
94
95         // Disable screenshots if not allowed.
96         if (!allowScreenshots) {
97             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
98         }
99
100         // Run the default commands.
101         super.onCreate(savedInstanceState)
102
103         // Get the launching intent
104         val intent = intent
105
106         // Get the information from the intent.
107         val currentUrl = intent.getStringExtra(CURRENT_URL)!!
108         val userAgent = intent.getStringExtra(USER_AGENT)!!
109
110         // Set the content view.
111         if (bottomAppBar) {
112             setContentView(R.layout.view_headers_bottom_appbar)
113         } else {
114             setContentView(R.layout.view_headers_top_appbar)
115         }
116
117         // Get a handle for the toolbar.
118         val toolbar = findViewById<Toolbar>(R.id.toolbar)
119
120         // Set the support action bar.
121         setSupportActionBar(toolbar)
122
123         // Get a handle for the action bar.
124         val actionBar = supportActionBar!!
125
126         // Add the custom layout to the action bar.
127         actionBar.setCustomView(R.layout.view_headers_appbar_custom_view)
128
129         // Instruct the action bar to display a custom layout.
130         actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
131
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)
148
149         // Populate the URL text box.
150         urlEditText.setText(currentUrl)
151
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))
156
157         // Apply text highlighting to the URL.
158         UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
159
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)
162
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)
173
174                 // Move to the beginning of the string.
175                 urlEditText.setSelection(0)
176
177                 // Reapply the highlighting.
178                 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
179             }
180         }
181
182         // Set the refresh color scheme according to the theme.
183         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
184
185         // Initialize a color background typed value.
186         val colorBackgroundTypedValue = TypedValue()
187
188         // Get the color background from the theme.
189         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
190
191         // Get the color background int from the typed value.
192         val colorBackgroundInt = colorBackgroundTypedValue.data
193
194         // Set the swipe refresh background color.
195         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
196
197         // Get the list of locales.
198         val localeList = resources.configuration.locales
199
200         // Initialize a string builder to extract the locales from the list.
201         val localesStringBuilder = StringBuilder()
202
203         // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
204         var q = 10
205
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.
209             if (i > 0) {
210                 localesStringBuilder.append(",")
211             }
212
213             // Get the locale from the list.
214             val locale = localeList[i]
215
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)
220
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.
222             if (q < 10) {
223                 localesStringBuilder.append(";q=0.")
224                 localesStringBuilder.append(q)
225             }
226
227             // Decrement `q` if it is greater than 1.
228             if (q > 1) {
229                 q--
230             }
231
232             // Add a second entry for the language only portion of the locale.
233             localesStringBuilder.append(",")
234             localesStringBuilder.append(locale.language)
235
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)
239
240             // Decrement `q` if it is greater than 1.
241             if (q > 1) {
242                 q--
243             }
244         }
245
246         // Instantiate the proxy helper.
247         val proxyHelper = ProxyHelper()
248
249         // Get the current proxy.
250         val proxy = proxyHelper.getCurrentProxy(this)
251
252         // Make the progress bar visible.
253         progressBar.visibility = View.VISIBLE
254
255         // Set the progress bar to be indeterminate.
256         progressBar.isIndeterminate = true
257
258         // Update the layout.
259         updateLayout(currentUrl)
260
261         // Instantiate the view headers factory.
262         val viewHeadersFactory: ViewModelProvider.Factory = ViewHeadersFactory(application, currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
263
264         // Instantiate the headers view model.
265         headersViewModel = ViewModelProvider(this, viewHeadersFactory)[HeadersViewModel::class.java]
266
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]
275
276             // Populate the dialog strings.
277             appliedCipherString = headersStringArray[1].toString()
278             availableCiphersString = headersStringArray[2].toString()
279             sslCertificateString = headersStringArray[3].toString()
280
281             // Hide the progress bar.
282             progressBar.isIndeterminate = false
283             progressBar.visibility = View.GONE
284
285             // Stop the swipe to refresh indicator if it is running
286             swipeRefreshLayout.isRefreshing = false
287         }
288
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()
296
297                     // Show the untrusted SSL certificate dialog.
298                     untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
299                 } else {
300                     // Display a snackbar with the error message.
301                     Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
302                 }
303             }
304         }
305
306         // Implement swipe to refresh.
307         swipeRefreshLayout.setOnRefreshListener {
308             // Make the progress bar visible.
309             progressBar.visibility = View.VISIBLE
310
311             // Set the progress bar to be indeterminate.
312             progressBar.isIndeterminate = true
313
314             // Get the URL.
315             val urlString = urlEditText.text.toString()
316
317             // Update the layout.
318             updateLayout(urlString)
319
320             // Get the updated headers.
321             headersViewModel.updateHeaders(urlString, false)
322         }
323
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)
330
331                 // Remove the focus from the URL box.
332                 urlEditText.clearFocus()
333
334                 // Make the progress bar visible.
335                 progressBar.visibility = View.VISIBLE
336
337                 // Set the progress bar to be indeterminate.
338                 progressBar.isIndeterminate = true
339
340                 // Get the URL.
341                 val urlString = urlEditText.text.toString()
342
343                 // Update the layout.
344                 updateLayout(urlString)
345
346                 // Get the updated headers.
347                 headersViewModel.updateHeaders(urlString, false)
348
349                 // Consume the key press.
350                 return@setOnKeyListener true
351             } else {
352                 // Do not consume the key press.
353                 return@setOnKeyListener false
354             }
355         }
356     }
357
358     override fun onCreateOptionsMenu(menu: Menu): Boolean {
359         // Inflate the menu.
360         menuInflater.inflate(R.menu.view_headers_options_menu, menu)
361
362         // Display the menu.
363         return true
364     }
365
366     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
367         // Instantiate the about dialog fragment.
368         val aboutDialogFragment: DialogFragment = AboutViewHeadersDialog()
369
370         // Show the about alert dialog.
371         aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
372
373         // Consume the event.
374         return true
375     }
376
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) {
379         // Go home.
380         NavUtils.navigateUpFromSameTask(this)
381     }
382
383     override fun loadAnyway() {
384         // Load the URL anyway.
385         headersViewModel.updateHeaders(urlEditText.text.toString(), true)
386     }
387
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)
392
393         // Show the dialog.
394         sslCertificateDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
395     }
396
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)
401
402         // Show the dialog.
403         ciphersDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
404     }
405
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
417
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
435             }
436
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
442
443             // Restore the text of the other title text views.
444             responseHeadersTitleTextView.setText(R.string.response_headers)
445             responseBodyTitleTextView.setText(R.string.response_body)
446         }
447     }
448 }