]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ViewSourceActivity.kt
Unify URL syntax highlighting. https://redmine.stoutner.com/issues/704
[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.style.ForegroundColorSpan
26 import android.util.TypedValue
27 import android.view.KeyEvent
28 import android.view.Menu
29 import android.view.MenuItem
30 import android.view.View
31 import android.view.View.OnFocusChangeListener
32 import android.view.WindowManager
33 import android.view.inputmethod.InputMethodManager
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.AboutViewSourceDialog
51 import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog
52 import com.stoutner.privacybrowser.dialogs.UntrustedSslCertificateDialog.UntrustedSslCertificateListener
53 import com.stoutner.privacybrowser.helpers.ProxyHelper
54 import com.stoutner.privacybrowser.helpers.UrlHelper
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         UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
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                 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
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 updateLayout(urlString: String) {
381         if (urlString.startsWith("content://")) {  // This is a content URL.
382             // Hide the unused text views.
383             requestHeadersTitleTextView.visibility = View.GONE
384             requestHeadersTextView.visibility = View.GONE
385             responseMessageTitleTextView.visibility = View.GONE
386             responseMessageTextView.visibility = View.GONE
387
388             // Change the text of the remaining title text views.
389             responseHeadersTitleTextView.setText(R.string.content_metadata)
390             responseBodyTitleTextView.setText(R.string.content_data)
391         } else {  // This is not a content URL.
392             // Show the views.
393             requestHeadersTitleTextView.visibility = View.VISIBLE
394             requestHeadersTextView.visibility = View.VISIBLE
395             responseMessageTitleTextView.visibility = View.VISIBLE
396             responseMessageTextView.visibility = View.VISIBLE
397
398             // Restore the text of the other title text views.
399             responseHeadersTitleTextView.setText(R.string.response_headers)
400             responseBodyTitleTextView.setText(R.string.response_body)
401         }
402     }
403 }