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