532eda553eb258e2c0809f8a14f762214b5a41b1
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ViewSourceActivity.kt
1 /*
2  * Copyright © 2017-2021 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.activities
21
22 import android.content.res.Configuration
23 import android.os.Build
24 import android.os.Bundle
25 import android.text.SpannableStringBuilder
26 import android.text.Spanned
27 import android.text.style.ForegroundColorSpan
28 import android.util.TypedValue
29 import android.view.KeyEvent
30 import android.view.Menu
31 import android.view.MenuItem
32 import android.view.View
33 import android.view.View.OnFocusChangeListener
34 import android.view.WindowManager
35 import android.view.inputmethod.InputMethodManager
36 import android.widget.EditText
37 import android.widget.ProgressBar
38 import android.widget.TextView
39
40 import androidx.appcompat.app.ActionBar
41 import androidx.appcompat.app.AppCompatActivity
42 import androidx.appcompat.widget.Toolbar
43 import androidx.core.app.NavUtils
44 import androidx.fragment.app.DialogFragment
45 import androidx.lifecycle.ViewModelProvider
46 import androidx.preference.PreferenceManager
47 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
48
49 import com.google.android.material.snackbar.Snackbar
50
51 import com.stoutner.privacybrowser.R
52 import com.stoutner.privacybrowser.dialogs.AboutViewSourceDialog
53 import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog
54 import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog.UntrustedSslCertificateListener
55 import com.stoutner.privacybrowser.helpers.ProxyHelper
56 import com.stoutner.privacybrowser.viewmodelfactories.WebViewSourceFactory
57 import com.stoutner.privacybrowser.viewmodels.WebViewSource
58
59 import java.util.Locale
60
61 // Declare the public constants.
62 const val CURRENT_URL = "current_url"
63 const val USER_AGENT = "user_agent"
64
65 class ViewSourceActivity: AppCompatActivity(), UntrustedSslCertificateListener {
66     // Declare the class variables.
67     private lateinit var initialGrayColorSpan: ForegroundColorSpan
68     private lateinit var finalGrayColorSpan: ForegroundColorSpan
69     private lateinit var redColorSpan: ForegroundColorSpan
70     private lateinit var webViewSource: WebViewSource
71
72     // Declare the class views.
73     private lateinit var urlEditText: EditText
74     private lateinit var requestHeadersTitleTextView: TextView
75     private lateinit var requestHeadersTextView: TextView
76     private lateinit var responseMessageTitleTextView: TextView
77     private lateinit var responseMessageTextView: TextView
78     private lateinit var responseHeadersTitleTextView: TextView
79     private lateinit var responseBodyTitleTextView: TextView
80
81     override fun onCreate(savedInstanceState: Bundle?) {
82         // Get a handle for the shared preferences.
83         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
84
85         // Get the preferences.
86         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
87         val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
88
89         // Disable screenshots if not allowed.
90         if (!allowScreenshots) {
91             window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
92         }
93
94         // Set the theme.
95         setTheme(R.style.PrivacyBrowser)
96
97         // Run the default commands.
98         super.onCreate(savedInstanceState)
99
100         // Get the launching intent
101         val intent = intent
102
103         // Get the information from the intent.
104         val currentUrl = intent.getStringExtra(CURRENT_URL)!!
105         val userAgent = intent.getStringExtra(USER_AGENT)!!
106
107         // Set the content view.
108         if (bottomAppBar) {
109             setContentView(R.layout.view_source_bottom_appbar)
110         } else {
111             setContentView(R.layout.view_source_top_appbar)
112         }
113
114         // Get a handle for the toolbar.
115         val toolbar = findViewById<Toolbar>(R.id.view_source_toolbar)
116
117         // Set the support action bar.
118         setSupportActionBar(toolbar)
119
120         // Get a handle for the action bar.
121         val actionBar = supportActionBar!!
122
123         // Add the custom layout to the action bar.
124         actionBar.setCustomView(R.layout.view_source_app_bar)
125
126         // Instruct the action bar to display a custom layout.
127         actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
128
129         // Get handles for the views.
130         urlEditText = findViewById(R.id.url_edittext)
131         val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
132         val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.view_source_swiperefreshlayout)
133         requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
134         requestHeadersTextView = findViewById(R.id.request_headers_textview)
135         responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
136         responseMessageTextView = findViewById(R.id.response_message_textview)
137         responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textivew)
138         val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
139         responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
140         val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
141
142         // Populate the URL text box.
143         urlEditText.setText(currentUrl)
144
145         // Initialize the gray foreground color spans for highlighting the URLs.  The deprecated `getColor()` must be used until the minimum API >= 23.
146         @Suppress("DEPRECATION")
147         initialGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
148         @Suppress("DEPRECATION")
149         finalGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
150
151         // Get the current theme status.
152         val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
153
154         // Set the red color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
155         redColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
156             @Suppress("DEPRECATION")
157             ForegroundColorSpan(resources.getColor(R.color.red_a700))
158         } else {
159             @Suppress("DEPRECATION")
160             ForegroundColorSpan(resources.getColor(R.color.red_900))
161         }
162
163         // Apply text highlighting to the URL.
164         highlightUrlText()
165
166         // Get a handle for the input method manager, which is used to hide the keyboard.
167         val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
168
169         // Remove the formatting from the URL when the user is editing the text.
170         urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
171             if (hasFocus) {  // The user is editing the URL text box.
172                 // Remove the highlighting.
173                 urlEditText.text.removeSpan(redColorSpan)
174                 urlEditText.text.removeSpan(initialGrayColorSpan)
175                 urlEditText.text.removeSpan(finalGrayColorSpan)
176             } else {  // The user has stopped editing the URL text box.
177                 // Hide the soft keyboard.
178                 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
179
180                 // Move to the beginning of the string.
181                 urlEditText.setSelection(0)
182
183                 // Reapply the highlighting.
184                 highlightUrlText()
185             }
186         }
187
188         // Set the refresh color scheme according to the theme.
189         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
190             swipeRefreshLayout.setColorSchemeResources(R.color.blue_700)
191         } else {
192             swipeRefreshLayout.setColorSchemeResources(R.color.violet_500)
193         }
194
195         // Initialize a color background typed value.
196         val colorBackgroundTypedValue = TypedValue()
197
198         // Get the color background from the theme.
199         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
200
201         // Get the color background int from the typed value.
202         val colorBackgroundInt = colorBackgroundTypedValue.data
203
204         // Set the swipe refresh background color.
205         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
206
207         // Populate the locale string.
208         val localeString = if (Build.VERSION.SDK_INT >= 24) {  // SDK >= 24 has a list of locales.
209             // Get the list of locales.
210             val localeList = resources.configuration.locales
211
212             // Initialize a string builder to extract the locales from the list.
213             val localesStringBuilder = StringBuilder()
214
215             // Initialize a `q` value, which is used by `WebView` to indicate the order of importance of the languages.
216             var q = 10
217
218             // Populate the string builder with the contents of the locales list.
219             for (i in 0 until localeList.size()) {
220                 // Append a comma if there is already an item in the string builder.
221                 if (i > 0) {
222                     localesStringBuilder.append(",")
223                 }
224
225                 // Get the locale from the list.
226                 val locale = localeList[i]
227
228                 // Add the locale to the string.  `locale` by default displays as `en_US`, but WebView uses the `en-US` format.
229                 localesStringBuilder.append(locale.language)
230                 localesStringBuilder.append("-")
231                 localesStringBuilder.append(locale.country)
232
233                 // If not the first locale, append `;q=0.x`, which drops by .1 for each removal from the main locale until q=0.1.
234                 if (q < 10) {
235                     localesStringBuilder.append(";q=0.")
236                     localesStringBuilder.append(q)
237                 }
238
239                 // Decrement `q` if it is greater than 1.
240                 if (q > 1) {
241                     q--
242                 }
243
244                 // Add a second entry for the language only portion of the locale.
245                 localesStringBuilder.append(",")
246                 localesStringBuilder.append(locale.language)
247
248                 // Append `1;q=0.x`, which drops by .1 for each removal form the main locale until q=0.1.
249                 localesStringBuilder.append(";q=0.")
250                 localesStringBuilder.append(q)
251
252                 // Decrement `q` if it is greater than 1.
253                 if (q > 1) {
254                     q--
255                 }
256             }
257
258             // Store the populated string builder in the locale string.
259             localesStringBuilder.toString()
260         } else {  // SDK < 24 only has a primary locale.
261             // Store the locale in the locale string.
262             Locale.getDefault().toString()
263         }
264
265         // Instantiate the proxy helper.
266         val proxyHelper = ProxyHelper()
267
268         // Get the current proxy.
269         val proxy = proxyHelper.getCurrentProxy(this)
270
271         // Make the progress bar visible.
272         progressBar.visibility = View.VISIBLE
273
274         // Set the progress bar to be indeterminate.
275         progressBar.isIndeterminate = true
276
277         // Update the layout.
278         updateLayout(currentUrl)
279
280         // Instantiate the WebView source factory.
281         val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl, userAgent, localeString, proxy, contentResolver, MainWebViewActivity.executorService)
282
283         // Instantiate the WebView source view model class.
284         webViewSource = ViewModelProvider(this, webViewSourceFactory).get(WebViewSource::class.java)
285
286         // Create a source observer.
287         webViewSource.observeSource().observe(this, { sourceStringArray: Array<SpannableStringBuilder> ->
288             // Populate the text views.  This can take a long time, and freezes the user interface, if the response body is particularly large.
289             requestHeadersTextView.text = sourceStringArray[0]
290             responseMessageTextView.text = sourceStringArray[1]
291             responseHeadersTextView.text = sourceStringArray[2]
292             responseBodyTextView.text = sourceStringArray[3]
293
294             // Hide the progress bar.
295             progressBar.isIndeterminate = false
296             progressBar.visibility = View.GONE
297
298             //Stop the swipe to refresh indicator if it is running
299             swipeRefreshLayout.isRefreshing = false
300         })
301
302         // Create an error observer.
303         webViewSource.observeErrors().observe(this, { errorString: String ->
304             // Display an error snackbar if the string is not `""`.
305             if (errorString != "") {
306                 if (errorString.startsWith("javax.net.ssl.SSLHandshakeException")) {
307                     // Instantiate the untrusted SSL certificate dialog.
308                     val untrustedSslCertificateDialog = UntrustedSslCertificateDialog()
309
310                     // Show the untrusted SSL certificate dialog.
311                     untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
312                 } else {
313                     // Display a snackbar with the error message.
314                     Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
315                 }
316             }
317         })
318
319         // Implement swipe to refresh.
320         swipeRefreshLayout.setOnRefreshListener {
321             // Make the progress bar visible.
322             progressBar.visibility = View.VISIBLE
323
324             // Set the progress bar to be indeterminate.
325             progressBar.isIndeterminate = true
326
327             // Get the URL.
328             val urlString = urlEditText.text.toString()
329
330             // Update the layout.
331             updateLayout(urlString)
332
333             // Get the updated source.
334             webViewSource.updateSource(urlString, false)
335         }
336
337         // Set the go button on the keyboard to request new source data.
338         urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
339             // Request new source data if the enter key was pressed.
340             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
341                 // Hide the soft keyboard.
342                 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
343
344                 // Remove the focus from the URL box.
345                 urlEditText.clearFocus()
346
347                 // Make the progress bar visible.
348                 progressBar.visibility = View.VISIBLE
349
350                 // Set the progress bar to be indeterminate.
351                 progressBar.isIndeterminate = true
352
353                 // Get the URL.
354                 val urlString = urlEditText.text.toString()
355
356                 // Update the layout.
357                 updateLayout(urlString)
358
359                 // Get the updated source.
360                 webViewSource.updateSource(urlString, false)
361
362                 // Consume the key press.
363                 return@setOnKeyListener true
364             } else {
365                 // Do not consume the key press.
366                 return@setOnKeyListener false
367             }
368         }
369     }
370
371     override fun onCreateOptionsMenu(menu: Menu): Boolean {
372         // Inflate the menu.
373         menuInflater.inflate(R.menu.view_source_options_menu, menu)
374
375         // Display the menu.
376         return true
377     }
378
379     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
380         // Instantiate the about dialog fragment.
381         val aboutDialogFragment: DialogFragment = AboutViewSourceDialog()
382
383         // Show the about alert dialog.
384         aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
385
386         // Consume the event.
387         return true
388     }
389
390     // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
391     fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
392         // Go home.
393         NavUtils.navigateUpFromSameTask(this)
394     }
395
396     override fun loadAnyway() {
397         // Load the URL anyway.
398         webViewSource.updateSource(urlEditText.text.toString(), true)
399     }
400
401     private fun highlightUrlText() {
402         // Get a handle for the URL edit text.
403         val urlEditText = findViewById<EditText>(R.id.url_edittext)
404
405         // Get the URL string.
406         val urlString = urlEditText.text.toString()
407
408         // Highlight the URL according to the protocol.
409         if (urlString.startsWith("file://")) {  // This is a file URL.
410             // De-emphasize only the protocol.
411             urlEditText.text.setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
412         } else if (urlString.startsWith("content://")) {
413             // De-emphasize only the protocol.
414             urlEditText.text.setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
415         } else {  // This is a web URL.
416             // Get the index of the `/` immediately after the domain name.
417             val endOfDomainName = urlString.indexOf("/", urlString.indexOf("//") + 2)
418
419             // Get the base URL.
420             val baseUrl = if (endOfDomainName > 0) {  // There is at least one character after the base URL.
421                 // Get the base URL.
422                 urlString.substring(0, endOfDomainName)
423             } else {  // There are no characters after the base URL.
424                 // Set the base URL to be the entire URL string.
425                 urlString
426             }
427
428             // Get the index of the last `.` in the domain.
429             val lastDotIndex = baseUrl.lastIndexOf(".")
430
431             // Get the index of the penultimate `.` in the domain.
432             val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1)
433
434             // Markup the beginning of the URL.
435             if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
436                 urlEditText.text.setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
437
438                 // De-emphasize subdomains.
439                 if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
440                     urlEditText.text.setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
441                 }
442             } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
443                 if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
444                     // De-emphasize the protocol and the additional subdomains.
445                     urlEditText.text.setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
446                 } else {  // There is only one subdomain in the domain name.
447                     // De-emphasize only the protocol.
448                     urlEditText.text.setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
449                 }
450             }
451
452             // De-emphasize the text after the domain name.
453             if (endOfDomainName > 0) {
454                 urlEditText.text.setSpan(finalGrayColorSpan, endOfDomainName, urlString.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
455             }
456         }
457     }
458
459     private fun updateLayout(urlString: String) {
460         if (urlString.startsWith("content://")) {  // This is a content URL.
461             // Hide the unused text views.
462             requestHeadersTitleTextView.visibility = View.GONE
463             requestHeadersTextView.visibility = View.GONE
464             responseMessageTitleTextView.visibility = View.GONE
465             responseMessageTextView.visibility = View.GONE
466
467             // Change the text of the remaining title text views.
468             responseHeadersTitleTextView.setText(R.string.content_metadata)
469             responseBodyTitleTextView.setText(R.string.content_data)
470         } else {  // This is not a content URL.
471             // Show the views.
472             requestHeadersTitleTextView.visibility = View.VISIBLE
473             requestHeadersTextView.visibility = View.VISIBLE
474             responseMessageTitleTextView.visibility = View.VISIBLE
475             responseMessageTextView.visibility = View.VISIBLE
476
477             // Restore the text of the other title text views.
478             responseHeadersTitleTextView.setText(R.string.response_headers)
479             responseBodyTitleTextView.setText(R.string.response_body)
480         }
481     }
482 }