]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt
e0a964ed203ebc35ea57f28cc241442301e2d209
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ViewSourceActivity.kt
1 /*
2  * Copyright © 2017-2022 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.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 // Define 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         // Run the default commands.
95         super.onCreate(savedInstanceState)
96
97         // Get the launching intent
98         val intent = intent
99
100         // Get the information from the intent.
101         val currentUrl = intent.getStringExtra(CURRENT_URL)!!
102         val userAgent = intent.getStringExtra(USER_AGENT)!!
103
104         // Set the content view.
105         if (bottomAppBar) {
106             setContentView(R.layout.view_source_bottom_appbar)
107         } else {
108             setContentView(R.layout.view_source_top_appbar)
109         }
110
111         // Get a handle for the toolbar.
112         val toolbar = findViewById<Toolbar>(R.id.view_source_toolbar)
113
114         // Set the support action bar.
115         setSupportActionBar(toolbar)
116
117         // Get a handle for the action bar.
118         val actionBar = supportActionBar!!
119
120         // Add the custom layout to the action bar.
121         actionBar.setCustomView(R.layout.view_source_appbar_custom_view)
122
123         // Instruct the action bar to display a custom layout.
124         actionBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
125
126         // Get handles for the views.
127         urlEditText = findViewById(R.id.url_edittext)
128         val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
129         val swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.view_source_swiperefreshlayout)
130         requestHeadersTitleTextView = findViewById(R.id.request_headers_title_textview)
131         requestHeadersTextView = findViewById(R.id.request_headers_textview)
132         responseMessageTitleTextView = findViewById(R.id.response_message_title_textview)
133         responseMessageTextView = findViewById(R.id.response_message_textview)
134         responseHeadersTitleTextView = findViewById(R.id.response_headers_title_textivew)
135         val responseHeadersTextView = findViewById<TextView>(R.id.response_headers_textview)
136         responseBodyTitleTextView = findViewById(R.id.response_body_title_textview)
137         val responseBodyTextView = findViewById<TextView>(R.id.response_body_textview)
138
139         // Populate the URL text box.
140         urlEditText.setText(currentUrl)
141
142         // Initialize the gray foreground color spans for highlighting the URLs.  The deprecated `getColor()` must be used until the minimum API >= 23.
143         @Suppress("DEPRECATION")
144         initialGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
145         @Suppress("DEPRECATION")
146         finalGrayColorSpan = ForegroundColorSpan(resources.getColor(R.color.gray_500))
147
148         // Get the current theme status.
149         val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
150
151         // Set the red color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
152         redColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
153             @Suppress("DEPRECATION")
154             ForegroundColorSpan(resources.getColor(R.color.red_a700))
155         } else {
156             @Suppress("DEPRECATION")
157             ForegroundColorSpan(resources.getColor(R.color.red_900))
158         }
159
160         // Apply text highlighting to the URL.
161         highlightUrlText()
162
163         // Get a handle for the input method manager, which is used to hide the keyboard.
164         val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
165
166         // Remove the formatting from the URL when the user is editing the text.
167         urlEditText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
168             if (hasFocus) {  // The user is editing the URL text box.
169                 // Remove the highlighting.
170                 urlEditText.text.removeSpan(redColorSpan)
171                 urlEditText.text.removeSpan(initialGrayColorSpan)
172                 urlEditText.text.removeSpan(finalGrayColorSpan)
173             } else {  // The user has stopped editing the URL text box.
174                 // Hide the soft keyboard.
175                 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
176
177                 // Move to the beginning of the string.
178                 urlEditText.setSelection(0)
179
180                 // Reapply the highlighting.
181                 highlightUrlText()
182             }
183         }
184
185         // Set the refresh color scheme according to the theme.
186         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
187
188         // Initialize a color background typed value.
189         val colorBackgroundTypedValue = TypedValue()
190
191         // Get the color background from the theme.
192         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
193
194         // Get the color background int from the typed value.
195         val colorBackgroundInt = colorBackgroundTypedValue.data
196
197         // Set the swipe refresh background color.
198         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
199
200         // Populate the locale string.
201         val localeString = if (Build.VERSION.SDK_INT >= 24) {  // SDK >= 24 has a list of locales.
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             // Store the populated string builder in the locale string.
252             localesStringBuilder.toString()
253         } else {  // SDK < 24 only has a primary locale.
254             // Store the locale in the locale string.
255             Locale.getDefault().toString()
256         }
257
258         // Instantiate the proxy helper.
259         val proxyHelper = ProxyHelper()
260
261         // Get the current proxy.
262         val proxy = proxyHelper.getCurrentProxy(this)
263
264         // Make the progress bar visible.
265         progressBar.visibility = View.VISIBLE
266
267         // Set the progress bar to be indeterminate.
268         progressBar.isIndeterminate = true
269
270         // Update the layout.
271         updateLayout(currentUrl)
272
273         // Instantiate the WebView source factory.
274         val webViewSourceFactory: ViewModelProvider.Factory = WebViewSourceFactory(currentUrl, userAgent, localeString, proxy, contentResolver, MainWebViewActivity.executorService)
275
276         // Instantiate the WebView source view model class.
277         webViewSource = ViewModelProvider(this, webViewSourceFactory).get(WebViewSource::class.java)
278
279         // Create a source observer.
280         webViewSource.observeSource().observe(this, { sourceStringArray: Array<SpannableStringBuilder> ->
281             // Populate the text views.  This can take a long time, and freezes the user interface, if the response body is particularly large.
282             requestHeadersTextView.text = sourceStringArray[0]
283             responseMessageTextView.text = sourceStringArray[1]
284             responseHeadersTextView.text = sourceStringArray[2]
285             responseBodyTextView.text = sourceStringArray[3]
286
287             // Hide the progress bar.
288             progressBar.isIndeterminate = false
289             progressBar.visibility = View.GONE
290
291             //Stop the swipe to refresh indicator if it is running
292             swipeRefreshLayout.isRefreshing = false
293         })
294
295         // Create an error observer.
296         webViewSource.observeErrors().observe(this, { errorString: String ->
297             // Display an error snackbar if the string is not `""`.
298             if (errorString != "") {
299                 if (errorString.startsWith("javax.net.ssl.SSLHandshakeException")) {
300                     // Instantiate the untrusted SSL certificate dialog.
301                     val untrustedSslCertificateDialog = UntrustedSslCertificateDialog()
302
303                     // Show the untrusted SSL certificate dialog.
304                     untrustedSslCertificateDialog.show(supportFragmentManager, getString(R.string.invalid_certificate))
305                 } else {
306                     // Display a snackbar with the error message.
307                     Snackbar.make(swipeRefreshLayout, errorString, Snackbar.LENGTH_LONG).show()
308                 }
309             }
310         })
311
312         // Implement swipe to refresh.
313         swipeRefreshLayout.setOnRefreshListener {
314             // Make the progress bar visible.
315             progressBar.visibility = View.VISIBLE
316
317             // Set the progress bar to be indeterminate.
318             progressBar.isIndeterminate = true
319
320             // Get the URL.
321             val urlString = urlEditText.text.toString()
322
323             // Update the layout.
324             updateLayout(urlString)
325
326             // Get the updated source.
327             webViewSource.updateSource(urlString, false)
328         }
329
330         // Set the go button on the keyboard to request new source data.
331         urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
332             // Request new source data if the enter key was pressed.
333             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
334                 // Hide the soft keyboard.
335                 inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
336
337                 // Remove the focus from the URL box.
338                 urlEditText.clearFocus()
339
340                 // Make the progress bar visible.
341                 progressBar.visibility = View.VISIBLE
342
343                 // Set the progress bar to be indeterminate.
344                 progressBar.isIndeterminate = true
345
346                 // Get the URL.
347                 val urlString = urlEditText.text.toString()
348
349                 // Update the layout.
350                 updateLayout(urlString)
351
352                 // Get the updated source.
353                 webViewSource.updateSource(urlString, false)
354
355                 // Consume the key press.
356                 return@setOnKeyListener true
357             } else {
358                 // Do not consume the key press.
359                 return@setOnKeyListener false
360             }
361         }
362     }
363
364     override fun onCreateOptionsMenu(menu: Menu): Boolean {
365         // Inflate the menu.
366         menuInflater.inflate(R.menu.view_source_options_menu, menu)
367
368         // Display the menu.
369         return true
370     }
371
372     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
373         // Instantiate the about dialog fragment.
374         val aboutDialogFragment: DialogFragment = AboutViewSourceDialog()
375
376         // Show the about alert dialog.
377         aboutDialogFragment.show(supportFragmentManager, getString(R.string.about))
378
379         // Consume the event.
380         return true
381     }
382
383     // This method must be named `goBack()` and must have a View argument to match the default back arrow in the app bar.
384     fun goBack(@Suppress("UNUSED_PARAMETER") view: View) {
385         // Go home.
386         NavUtils.navigateUpFromSameTask(this)
387     }
388
389     override fun loadAnyway() {
390         // Load the URL anyway.
391         webViewSource.updateSource(urlEditText.text.toString(), true)
392     }
393
394     private fun highlightUrlText() {
395         // Get a handle for the URL edit text.
396         val urlEditText = findViewById<EditText>(R.id.url_edittext)
397
398         // Get the URL string.
399         val urlString = urlEditText.text.toString()
400
401         // Highlight the URL according to the protocol.
402         if (urlString.startsWith("file://")) {  // This is a file URL.
403             // De-emphasize only the protocol.
404             urlEditText.text.setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
405         } else if (urlString.startsWith("content://")) {
406             // De-emphasize only the protocol.
407             urlEditText.text.setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
408         } else {  // This is a web URL.
409             // Get the index of the `/` immediately after the domain name.
410             val endOfDomainName = urlString.indexOf("/", urlString.indexOf("//") + 2)
411
412             // Get the base URL.
413             val baseUrl = if (endOfDomainName > 0) {  // There is at least one character after the base URL.
414                 // Get the base URL.
415                 urlString.substring(0, endOfDomainName)
416             } else {  // There are no characters after the base URL.
417                 // Set the base URL to be the entire URL string.
418                 urlString
419             }
420
421             // Get the index of the last `.` in the domain.
422             val lastDotIndex = baseUrl.lastIndexOf(".")
423
424             // Get the index of the penultimate `.` in the domain.
425             val penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1)
426
427             // Markup the beginning of the URL.
428             if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
429                 urlEditText.text.setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
430
431                 // De-emphasize subdomains.
432                 if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
433                     urlEditText.text.setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
434                 }
435             } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
436                 if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
437                     // De-emphasize the protocol and the additional subdomains.
438                     urlEditText.text.setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
439                 } else {  // There is only one subdomain in the domain name.
440                     // De-emphasize only the protocol.
441                     urlEditText.text.setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
442                 }
443             }
444
445             // De-emphasize the text after the domain name.
446             if (endOfDomainName > 0) {
447                 urlEditText.text.setSpan(finalGrayColorSpan, endOfDomainName, urlString.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
448             }
449         }
450     }
451
452     private fun updateLayout(urlString: String) {
453         if (urlString.startsWith("content://")) {  // This is a content URL.
454             // Hide the unused text views.
455             requestHeadersTitleTextView.visibility = View.GONE
456             requestHeadersTextView.visibility = View.GONE
457             responseMessageTitleTextView.visibility = View.GONE
458             responseMessageTextView.visibility = View.GONE
459
460             // Change the text of the remaining title text views.
461             responseHeadersTitleTextView.setText(R.string.content_metadata)
462             responseBodyTitleTextView.setText(R.string.content_data)
463         } else {  // This is not a content URL.
464             // Show the views.
465             requestHeadersTitleTextView.visibility = View.VISIBLE
466             requestHeadersTextView.visibility = View.VISIBLE
467             responseMessageTitleTextView.visibility = View.VISIBLE
468             responseMessageTextView.visibility = View.VISIBLE
469
470             // Restore the text of the other title text views.
471             responseHeadersTitleTextView.setText(R.string.response_headers)
472             responseBodyTitleTextView.setText(R.string.response_body)
473         }
474     }
475 }