]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ViewHeadersActivity.kt
145ced5d4d1e9ee568a523fe44d1801487bf84e2
[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         // 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))
153
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)
156
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)
162
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)
169
170                 // Move to the beginning of the string.
171                 urlEditText.setSelection(0)
172
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())
175
176                 // Reapply the highlighting.
177                 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
178             }
179         }
180
181         // Populate the URL text box.
182         urlEditText.setText(currentUrl)
183
184         // Apply the initial text highlighting to the URL.
185         UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
186
187         // Set the refresh color scheme according to the theme.
188         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
189
190         // Initialize a color background typed value.
191         val colorBackgroundTypedValue = TypedValue()
192
193         // Get the color background from the theme.
194         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
195
196         // Get the color background int from the typed value.
197         val colorBackgroundInt = colorBackgroundTypedValue.data
198
199         // Set the swipe refresh background color.
200         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
201
202         // Get the list of locales.
203         val localeList = resources.configuration.locales
204
205         // Initialize a string builder to extract the locales from the list.
206         val localesStringBuilder = StringBuilder()
207
208         // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
209         var q = 10
210
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.
214             if (i > 0) {
215                 localesStringBuilder.append(",")
216             }
217
218             // Get the locale from the list.
219             val locale = localeList[i]
220
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)
225
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.
227             if (q < 10) {
228                 localesStringBuilder.append(";q=0.")
229                 localesStringBuilder.append(q)
230             }
231
232             // Decrement `q` if it is greater than 1.
233             if (q > 1) {
234                 q--
235             }
236
237             // Add a second entry for the language only portion of the locale.
238             localesStringBuilder.append(",")
239             localesStringBuilder.append(locale.language)
240
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)
244
245             // Decrement `q` if it is greater than 1.
246             if (q > 1) {
247                 q--
248             }
249         }
250
251         // Instantiate the proxy helper.
252         val proxyHelper = ProxyHelper()
253
254         // Get the current proxy.
255         val proxy = proxyHelper.getCurrentProxy(this)
256
257         // Make the progress bar visible.
258         progressBar.visibility = View.VISIBLE
259
260         // Set the progress bar to be indeterminate.
261         progressBar.isIndeterminate = true
262
263         // Update the layout.
264         updateLayout(currentUrl)
265
266         // Instantiate the view headers factory.
267         val viewHeadersFactory: ViewModelProvider.Factory = ViewHeadersFactory(application, currentUrl, userAgent, localesStringBuilder.toString(), proxy, contentResolver, MainWebViewActivity.executorService)
268
269         // Instantiate the headers view model.
270         headersViewModel = ViewModelProvider(this, viewHeadersFactory)[HeadersViewModel::class.java]
271
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]
280
281             // Populate the dialog strings.
282             appliedCipherString = headersStringArray[1].toString()
283             availableCiphersString = headersStringArray[2].toString()
284             sslCertificateString = headersStringArray[3].toString()
285
286             // Hide the progress bar.
287             progressBar.isIndeterminate = false
288             progressBar.visibility = View.GONE
289
290             // Stop the swipe to refresh indicator if it is running
291             swipeRefreshLayout.isRefreshing = false
292         }
293
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()
301
302                     // Show the untrusted SSL certificate dialog.
303                     untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
304                 } else {
305                     // Display a snackbar with the error message.
306                     Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
307                 }
308             }
309         }
310
311         // Implement swipe to refresh.
312         swipeRefreshLayout.setOnRefreshListener {
313             // Make the progress bar visible.
314             progressBar.visibility = View.VISIBLE
315
316             // Set the progress bar to be indeterminate.
317             progressBar.isIndeterminate = true
318
319             // Get the URL.
320             val urlString = urlEditText.text.toString()
321
322             // Update the layout.
323             updateLayout(urlString)
324
325             // Get the updated headers.
326             headersViewModel.updateHeaders(urlString, false)
327         }
328
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)
335
336                 // Remove the focus from the URL box.
337                 urlEditText.clearFocus()
338
339                 // Make the progress bar visible.
340                 progressBar.visibility = View.VISIBLE
341
342                 // Set the progress bar to be indeterminate.
343                 progressBar.isIndeterminate = true
344
345                 // Get the URL.
346                 val urlString = urlEditText.text.toString()
347
348                 // Update the layout.
349                 updateLayout(urlString)
350
351                 // Get the updated headers.
352                 headersViewModel.updateHeaders(urlString, false)
353
354                 // Consume the key press.
355                 return@setOnKeyListener true
356             } else {
357                 // Do not consume the key press.
358                 return@setOnKeyListener false
359             }
360         }
361     }
362
363     override fun onCreateOptionsMenu(menu: Menu): Boolean {
364         // Inflate the menu.
365         menuInflater.inflate(R.menu.view_headers_options_menu, menu)
366
367         // Display the menu.
368         return true
369     }
370
371     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
372         // Instantiate the about dialog fragment.
373         val aboutDialogFragment: DialogFragment = AboutViewHeadersDialog()
374
375         // Show the about alert dialog.
376         aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
377
378         // Consume the event.
379         return true
380     }
381
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) {
384         // Go home.
385         NavUtils.navigateUpFromSameTask(this)
386     }
387
388     override fun loadAnyway() {
389         // Load the URL anyway.
390         headersViewModel.updateHeaders(urlEditText.text.toString(), true)
391     }
392
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)
397
398         // Show the dialog.
399         sslCertificateDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
400     }
401
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)
406
407         // Show the dialog.
408         ciphersDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate))
409     }
410
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
422
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
440             }
441
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
447
448             // Restore the text of the other title text views.
449             responseHeadersTitleTextView.setText(R.string.response_headers)
450             responseBodyTitleTextView.setText(R.string.response_body)
451         }
452     }
453 }