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